diff --git a/pyproject.toml b/pyproject.toml index ec8c236..9072184 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,17 +2,19 @@ name = "liquidonnx" version = "0.1.0" description = "LFM2 ONNX export and inference tools" -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = [ "onnx", "onnxruntime>=1.24.0.dev", # Nightly for QMoE 14-input support "transformers>=5.0.0.dev0", - "numpy>=2.2.0", + "numpy>=2.2.0,<2.4", # numba requires numpy < 2.4 "torch>=2.0.0", "pillow", "torchvision>=0.24.1", "onnx-ir>=0.1.13", + "scipy>=1.12.0", # For ISTFT in audio decoding + "onnxscript>=0.5.7", ] [project.optional-dependencies] @@ -22,6 +24,7 @@ gpu = [ dev = [ "pytest", "ruff", + "liquid-audio>=1.1.0", # Reference model for audio export/tests ] [project.scripts] @@ -38,6 +41,10 @@ lfm2-vl-infer = "liquidonnx.lfm2_vl.infer:main" lfm2-moe-export = "liquidonnx.lfm2_moe.export:main" lfm2-moe-infer = "liquidonnx.lfm2_moe.infer:main" +# LFM2.5-Audio model tools +lfm2-audio-export = "liquidonnx.lfm2_audio.export:main" +lfm2-audio-infer = "liquidonnx.lfm2_audio.infer:main" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" @@ -53,7 +60,7 @@ log_cli_level = "INFO" [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py312" [tool.ruff.lint] select = [ @@ -84,3 +91,10 @@ explicit = true transformers = { git = "https://github.com/huggingface/transformers.git", rev = "3c25177" } onnxruntime = { index = "ort-nightly" } onnxruntime-gpu = { index = "ort-nightly" } + +[dependency-groups] +dev = [ + "pytest>=9.0.2", + "ruff>=0.14.10", + "liquid-audio>=1.1.0", +] diff --git a/samples/audio/fool_me_once_mono.wav b/samples/audio/fool_me_once_mono.wav new file mode 100644 index 0000000..461bcc2 Binary files /dev/null and b/samples/audio/fool_me_once_mono.wav differ diff --git a/samples/audio/woodworks_question.wav b/samples/audio/woodworks_question.wav new file mode 100644 index 0000000..e2a95e8 Binary files /dev/null and b/samples/audio/woodworks_question.wav differ diff --git a/scripts/asr_liquidaudio.py b/scripts/asr_liquidaudio.py new file mode 100644 index 0000000..01f2af0 --- /dev/null +++ b/scripts/asr_liquidaudio.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Generate ASR transcription using liquid-audio (PyTorch).""" + +import argparse +import logging + +import numpy as np +import torch +from liquid_audio import ChatState, LFM2AudioModel, LFM2AudioProcessor +from scipy.io import wavfile + +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser(description="Generate ASR using liquid-audio") + parser.add_argument( + "audio", + help="Path to audio file to transcribe", + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + device = "cuda" if torch.cuda.is_available() else "cpu" + logger.info(f"Using device: {device}") + + # Load model and processor + logger.info("Loading liquid-audio model...") + model = LFM2AudioModel.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + dtype=torch.bfloat16 if device == "cuda" else torch.float32, + device=device, + ) + model.eval() # Disable dropout for inference + + logger.info("Loading processor...") + processor = LFM2AudioProcessor.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + device=device, + ) + + # Load audio file + logger.info(f"Loading audio: {args.audio}") + sample_rate, audio_data = wavfile.read(args.audio) + logger.info(f"Audio sample rate: {sample_rate}, shape: {audio_data.shape}") + + # Convert to float32 tensor normalized to [-1, 1] + if audio_data.dtype == np.int16: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 32768.0 + elif audio_data.dtype == np.int32: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 2147483648.0 + else: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) + + # Add batch dimension: [samples] → [1, samples] + audio_tensor = audio_tensor.unsqueeze(0).to(device) + logger.info( + f"Audio tensor shape: {audio_tensor.shape}, range: [{audio_tensor.min():.3f}, {audio_tensor.max():.3f}]" + ) + + # Set random seed for reproducibility (after model loading to ensure consistency) + seed = 42 + torch.manual_seed(seed) + np.random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + # Create chat state for ASR + dtype = torch.bfloat16 if device == "cuda" else torch.float32 + state = ChatState(processor, dtype=dtype) + + # System instruction for ASR + state.new_turn("system") + state.add_text("Perform ASR.") + state.end_turn() + + # User message with audio + state.new_turn("user") + state.add_audio(audio_tensor, sample_rate) + state.end_turn() + + # Start assistant turn (model will generate transcription) + state.new_turn("assistant") + + # Generate text tokens + logger.info("Generating transcription...") + max_tokens = 200 + + # ASR uses greedy decoding (temperature=None) for deterministic output + generator = model.generate_sequential( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=max_tokens, + text_temperature=None, # Greedy decoding for ASR + audio_temperature=None, + ) + + generated_tokens = [] + for i, token in enumerate(generator): + token_id = token.item() if token.numel() == 1 else token[0].item() + + # Check for end tokens + if token_id == 7: # <|im_end|> + logger.info(f"End of turn token received at position {i}") + break + + # Skip audio start token (ASR should only generate text) + if token_id == 128: # <|audio_start|> + logger.info("Skipping audio_start token") + continue + + generated_tokens.append(token_id) + + if len(generated_tokens) % 20 == 0: + logger.info(f"Generated {len(generated_tokens)} tokens...") + + # Decode tokens to text + transcription = processor.text.decode(generated_tokens, skip_special_tokens=True) + + logger.info(f"Generated {len(generated_tokens)} tokens") + print("\n" + "=" * 60) + print(f"Audio: {args.audio}") + print(f"Transcription: {transcription}") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/scripts/asr_liquidaudio.sh b/scripts/asr_liquidaudio.sh new file mode 100755 index 0000000..c08f836 --- /dev/null +++ b/scripts/asr_liquidaudio.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run scripts/asr_liquidaudio.py samples/audio/fool_me_once_mono.wav \ + | tee output/asr_liquidaudio.txt diff --git a/scripts/asr_onnx.sh b/scripts/asr_onnx.sh new file mode 100755 index 0000000..bb6be44 --- /dev/null +++ b/scripts/asr_onnx.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode asr \ + --audio samples/audio/fool_me_once_mono.wav \ + | tee output/asr_onnx.txt diff --git a/scripts/asr_onnx_fp16.sh b/scripts/asr_onnx_fp16.sh new file mode 100755 index 0000000..3eeae50 --- /dev/null +++ b/scripts/asr_onnx_fp16.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode asr \ + --precision fp16 \ + --audio samples/audio/fool_me_once_mono.wav \ + | tee output/asr_onnx_fp16.txt diff --git a/scripts/asr_onnx_q4.sh b/scripts/asr_onnx_q4.sh new file mode 100755 index 0000000..4b112ec --- /dev/null +++ b/scripts/asr_onnx_q4.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode asr \ + --precision q4 \ + --audio samples/audio/fool_me_once_mono.wav \ + | tee output/asr_onnx_q4.txt diff --git a/scripts/asr_onnx_q8.sh b/scripts/asr_onnx_q8.sh new file mode 100755 index 0000000..273de65 --- /dev/null +++ b/scripts/asr_onnx_q8.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode asr \ + --precision q8 \ + --audio samples/audio/fool_me_once_mono.wav \ + | tee output/asr_onnx_q8.txt diff --git a/scripts/interleaved_liquidaudio.py b/scripts/interleaved_liquidaudio.py new file mode 100644 index 0000000..2f92b4f --- /dev/null +++ b/scripts/interleaved_liquidaudio.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""Generate interleaved text+audio response using liquid-audio (PyTorch). + +Supports two decoders: +- mimi: Streaming neural codec (used in official chat.py demo) +- detokenizer: LFM2AudioDetokenizer (used in processor.decode(), matches ONNX) +""" + +import argparse +import logging + +import numpy as np +import torch +from liquid_audio import ChatState, LFM2AudioModel, LFM2AudioProcessor +from scipy.io import wavfile + +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser(description="Generate interleaved response using liquid-audio") + parser.add_argument( + "audio", + help="Path to input audio file", + ) + parser.add_argument( + "--output", + default="interleaved_liquidaudio.wav", + help="Output WAV file", + ) + parser.add_argument( + "--decoder", + choices=["mimi", "detokenizer"], + default="detokenizer", + help="Audio decoder: mimi (official demo) or detokenizer (ONNX-compatible)", + ) + parser.add_argument( + "--save-codes", + type=str, + metavar="FILE", + help="Save audio codes to numpy file (.npy) for comparison with other decoders", + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + # Select best available device: CUDA > MPS > CPU + if torch.cuda.is_available(): + device = "cuda" + gpu_name = torch.cuda.get_device_name(0) + logger.info(f"Using device: {device} ({gpu_name})") + elif torch.backends.mps.is_available(): + device = "mps" + logger.info(f"Using device: {device} (Apple Silicon GPU)") + else: + device = "cpu" + logger.info(f"Using device: {device} (no GPU available)") + logger.info(f"Decoder: {args.decoder}") + + # Load model and processor + # Use bfloat16 on CUDA for speed, float32 on MPS/CPU (MPS bfloat16 support is limited) + dtype = torch.bfloat16 if device == "cuda" else torch.float32 + logger.info(f"Loading liquid-audio model (dtype={dtype})...") + model = LFM2AudioModel.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + dtype=dtype, + device=device, + ) + model.eval() # Disable dropout for inference + + logger.info("Loading processor...") + processor = LFM2AudioProcessor.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + device=device, + ) + + # Pre-initialize mimi to ensure consistent random state regardless of decoder choice + # (processor.mimi is lazily loaded and consumes random numbers on first access) + _ = processor.mimi + + # Load audio file + logger.info(f"Loading audio: {args.audio}") + sample_rate, audio_data = wavfile.read(args.audio) + logger.info(f"Audio sample rate: {sample_rate}, shape: {audio_data.shape}") + + # Convert to float32 tensor normalized to [-1, 1] + if audio_data.dtype == np.int16: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 32768.0 + elif audio_data.dtype == np.int32: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 2147483648.0 + else: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) + + # Add batch dimension: [samples] → [1, samples] + audio_tensor = audio_tensor.unsqueeze(0).to(device) + + # Set random seed for reproducibility (after model loading to ensure consistency) + seed = 42 + torch.manual_seed(seed) + np.random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + # Create chat state for interleaved dialogue (reuses dtype from model loading) + state = ChatState(processor, dtype=dtype) + + # System instruction for interleaved dialogue (matching official demo) + state.new_turn("system") + state.add_text("Respond with interleaved text and audio.") + state.end_turn() + + # User message with audio + state.new_turn("user") + state.add_audio(audio_tensor, sample_rate) + state.end_turn() + + # Start assistant turn + state.new_turn("assistant") + + # Generate interleaved text + audio + logger.info("Generating interleaved response...") + max_tokens = 300 + + text_tokens = [] + audio_frames = [] + audio_chunks = [] # For mimi streaming + + if args.decoder == "mimi": + # === Mimi streaming decoder (official demo style) === + mimi = processor.mimi.eval() + + with torch.no_grad(), mimi.streaming(1): + generator = model.generate_interleaved( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=max_tokens, + text_temperature=1.0, + audio_temperature=1.0, + audio_top_k=4, + ) + + for i, token in enumerate(generator): + if token.numel() == 1: + # Text token + token_id = token.item() + if token_id == 7: # <|im_end|> + logger.info(f"End of turn at position {i}") + break + if token_id == 130: # <|text_end|> + logger.info(f"Text end at position {i}") + continue + text_tokens.append(token_id) + elif token.numel() == 8: + # Audio frame (8 codebooks) + if (token == 2048).any(): + logger.info(f"Skipping frame with 2048 at position {i}") + continue + + frame = token.cpu().numpy().flatten() + audio_frames.append(frame) + + # Decode immediately with mimi (streaming) + wav_chunk = mimi.decode(token[None, :, None])[0] + audio_chunks.append(wav_chunk.cpu()) + + if (len(text_tokens) + len(audio_frames)) % 50 == 0: + logger.info( + f"Generated {len(text_tokens)} text tokens, {len(audio_frames)} audio frames..." + ) + + else: + # === Detokenizer batch decoder (ONNX-compatible) === + with torch.no_grad(): + generator = model.generate_interleaved( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=max_tokens, + text_temperature=1.0, + audio_temperature=1.0, + audio_top_k=4, + ) + + for i, token in enumerate(generator): + if token.numel() == 1: + # Text token + token_id = token.item() + if token_id == 7: # <|im_end|> + logger.info(f"End of turn at position {i}") + break + if token_id == 130: # <|text_end|> + logger.info(f"Text end at position {i}") + continue + text_tokens.append(token_id) + elif token.numel() == 8: + # Audio frame (8 codebooks) + if (token == 2048).any(): + logger.info(f"Skipping frame with 2048 at position {i}") + continue + + frame = token.cpu().numpy().flatten() + audio_frames.append(frame) + + if (len(text_tokens) + len(audio_frames)) % 50 == 0: + logger.info( + f"Generated {len(text_tokens)} text tokens, {len(audio_frames)} audio frames..." + ) + + # Decode text + transcription = processor.text.decode(text_tokens, skip_special_tokens=True) + logger.info(f"Generated {len(text_tokens)} text tokens, {len(audio_frames)} audio frames") + + print("\n" + "=" * 60) + print(f"Audio input: {args.audio}") + print(f"Decoder: {args.decoder}") + print(f"Text response: {transcription}") + print(f"Audio frames: {len(audio_frames)}") + + # Save audio codes for comparison + if audio_frames: + audio_codes = np.stack(audio_frames, axis=0) # [T, 8] + audio_codes = np.clip(audio_codes, 0, 2047) + if args.save_codes: + np.save(args.save_codes, audio_codes) + print(f"Codes: {args.save_codes} {audio_codes.shape}") + + # Decode and save audio + if args.decoder == "mimi" and audio_chunks: + # Concatenate mimi streaming chunks + waveform = torch.cat(audio_chunks, dim=-1).squeeze().numpy() + logger.info( + f"Mimi waveform shape: {waveform.shape}, duration: {len(waveform) / 24000:.2f}s" + ) + + # Normalize and save + max_val = np.abs(waveform).max() + if max_val > 0: + waveform = waveform / max_val * 0.9 + + wavfile.write(args.output, 24000, (waveform * 32767).astype(np.int16)) + logger.info(f"Saved audio: {args.output}") + print(f"Audio output: {args.output}") + + elif args.decoder == "detokenizer" and audio_frames: + # Batch decode with LFM2AudioDetokenizer + detokenizer = processor.audio_detokenizer.to(device).eval() + + codes_tensor = torch.tensor(audio_codes.T, dtype=torch.long, device=device).unsqueeze(0) + logger.info(f"Audio codes shape: {codes_tensor.shape}") + + with torch.no_grad(): + waveform = detokenizer(codes_tensor) + + waveform = waveform.squeeze().cpu().numpy() + logger.info( + f"Detokenizer waveform shape: {waveform.shape}, duration: {len(waveform) / 24000:.2f}s" + ) + + # Normalize and save + max_val = np.abs(waveform).max() + if max_val > 0: + waveform = waveform / max_val * 0.9 + + wavfile.write(args.output, 24000, (waveform * 32767).astype(np.int16)) + logger.info(f"Saved audio: {args.output}") + print(f"Audio output: {args.output}") + + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/scripts/interleaved_liquidaudio.sh b/scripts/interleaved_liquidaudio.sh new file mode 100755 index 0000000..3a47fcc --- /dev/null +++ b/scripts/interleaved_liquidaudio.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Generate interleaved audio using liquid-audio with detokenizer (ONNX-compatible) +set -e +set -x +mkdir -p output +uv run scripts/interleaved_liquidaudio.py samples/audio/woodworks_question.wav \ + --decoder detokenizer \ + --output output/interleaved_liquidaudio.wav \ + --save-codes output/interleaved_liquidaudio_codes.npy diff --git a/scripts/interleaved_liquidaudio_mimi.sh b/scripts/interleaved_liquidaudio_mimi.sh new file mode 100755 index 0000000..e672595 --- /dev/null +++ b/scripts/interleaved_liquidaudio_mimi.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Generate interleaved audio using liquid-audio with mimi decoder (official demo style) +set -e +set -x +mkdir -p output +uv run scripts/interleaved_liquidaudio.py samples/audio/woodworks_question.wav \ + --decoder mimi \ + --output output/interleaved_liquidaudio_mimi.wav \ + --save-codes output/interleaved_liquidaudio_mimi_codes.npy diff --git a/scripts/interleaved_onnx.sh b/scripts/interleaved_onnx.sh new file mode 100755 index 0000000..dbdde02 --- /dev/null +++ b/scripts/interleaved_onnx.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode interleaved \ + --audio samples/audio/woodworks_question.wav \ + --output output/interleaved_onnx.wav \ + --save-codes output/interleaved_onnx_codes.npy \ + --seed 42 diff --git a/scripts/interleaved_onnx_fp16.sh b/scripts/interleaved_onnx_fp16.sh new file mode 100755 index 0000000..940e1fc --- /dev/null +++ b/scripts/interleaved_onnx_fp16.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode interleaved \ + --precision fp16 \ + --audio samples/audio/woodworks_question.wav \ + --output output/interleaved_onnx_fp16.wav \ + --save-codes output/interleaved_onnx_fp16_codes.npy \ + --seed 42 diff --git a/scripts/interleaved_onnx_q4.sh b/scripts/interleaved_onnx_q4.sh new file mode 100755 index 0000000..1f283df --- /dev/null +++ b/scripts/interleaved_onnx_q4.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode interleaved \ + --precision q4 \ + --audio samples/audio/woodworks_question.wav \ + --output output/interleaved_onnx_q4.wav \ + --save-codes output/interleaved_onnx_q4_codes.npy \ + --seed 42 diff --git a/scripts/interleaved_onnx_q8.sh b/scripts/interleaved_onnx_q8.sh new file mode 100755 index 0000000..18c1e82 --- /dev/null +++ b/scripts/interleaved_onnx_q8.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode interleaved \ + --precision q8 \ + --audio samples/audio/woodworks_question.wav \ + --output output/interleaved_onnx_q8.wav \ + --save-codes output/interleaved_onnx_q8_codes.npy \ + --seed 42 diff --git a/scripts/tts_liquidaudio.py b/scripts/tts_liquidaudio.py new file mode 100644 index 0000000..fda634d --- /dev/null +++ b/scripts/tts_liquidaudio.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Generate TTS using liquid-audio (PyTorch) to verify it works correctly.""" + +import argparse +import logging + +import numpy as np +import torch +from liquid_audio import ChatState, LFM2AudioModel, LFM2AudioProcessor +from scipy.io import wavfile + +logger = logging.getLogger(__name__) + + +def main(): + parser = argparse.ArgumentParser(description="Generate TTS using liquid-audio") + parser.add_argument( + "text", + nargs="?", + default="Hello! This is a test of the text to speech system.", + help="Text to synthesize", + ) + parser.add_argument( + "--output", + default="tts_liquidaudio.wav", + help="Output WAV file", + ) + parser.add_argument( + "--system", + default="Perform TTS. Use the UK female voice.", + help="System prompt for TTS", + ) + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + device = "cuda" if torch.cuda.is_available() else "cpu" + logger.info(f"Using device: {device}") + + # Load model and processor + logger.info("Loading liquid-audio model...") + model = LFM2AudioModel.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + dtype=torch.bfloat16 if device == "cuda" else torch.float32, + device=device, + ) + model.eval() # Disable dropout for inference + + logger.info("Loading processor...") + processor = LFM2AudioProcessor.from_pretrained( + "LiquidAI/LFM2.5-Audio-1.5B", + device=device, + ) + + # Set random seed for reproducibility (after model loading to ensure consistency) + seed = 42 + torch.manual_seed(seed) + np.random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + # Create chat state for TTS + text = args.text + logger.info(f"Input text: '{text}'") + + dtype = torch.bfloat16 if device == "cuda" else torch.float32 + state = ChatState(processor, dtype=dtype) + + # System instruction for TTS + state.new_turn("system") + state.add_text(args.system) + state.end_turn() + logger.info(f"System prompt: '{args.system}'") + + # User message with text to speak + state.new_turn("user") + state.add_text(text) + state.end_turn() + + # Start assistant turn (model will generate audio) + state.new_turn("assistant") + + # Generate audio tokens + logger.info("Generating audio tokens...") + audio_frames = [] + max_frames = 300 + + # Use generate_sequential for TTS (not interleaved which is for dialogue) + # generate_sequential: text until <|audio_start|> then continuous audio + generator = model.generate_sequential( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=max_frames, + text_temperature=0.7, + audio_temperature=0.7, + ) + + for token in generator: + # token shape: [8] or similar + token_np = token.cpu().numpy().flatten() + + # Check for end-of-audio token (2048) + if len(token_np) > 0 and token_np[0] == 2048: + logger.info("End-of-audio token received") + break + + # Only append audio tokens (8 codebooks) + if len(token_np) == 8: + audio_frames.append(token_np) + + if len(audio_frames) % 50 == 0: + logger.info(f"Generated {len(audio_frames)} frames...") + + logger.info(f"Total audio frames: {len(audio_frames)}") + + if not audio_frames: + logger.error("No audio frames generated!") + return + + # Stack into [T, 8] + audio_codes = np.stack(audio_frames, axis=0) + audio_codes = np.clip(audio_codes, 0, 2047) + logger.info(f"Audio codes shape: {audio_codes.shape}") + + # Save codes + np.save("tts_codes_liquidaudio_fresh.npy", audio_codes) + + # Decode with processor + # [T, 8] → [1, 8, T] + codes_tensor = torch.tensor(audio_codes.T, dtype=torch.int64).unsqueeze(0) + codes_tensor = codes_tensor.to(device) + + logger.info("Decoding audio...") + with torch.no_grad(): + waveform = processor.decode(codes_tensor).cpu().numpy()[0] + + logger.info(f"Waveform shape: {waveform.shape}") + logger.info(f"Waveform stats: min={waveform.min():.4f}, max={waveform.max():.4f}") + logger.info(f"RMS: {np.sqrt(np.mean(waveform**2)):.4f}") + + # Save audio at 24kHz + sample_rate = 24000 + duration = len(waveform) / sample_rate + logger.info(f"Duration: {duration:.2f}s") + + # Normalize and save + max_val = np.abs(waveform).max() + if max_val > 0: + normalized = waveform / max_val * 0.9 + else: + normalized = waveform + + wavfile.write(args.output, sample_rate, (normalized * 32767).astype(np.int16)) + logger.info(f"Saved: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/tts_liquidaudio.sh b/scripts/tts_liquidaudio.sh new file mode 100755 index 0000000..3056ec8 --- /dev/null +++ b/scripts/tts_liquidaudio.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +set -x +mkdir -p output + +SYSTEM_PROMPT="Perform TTS. Use the UK female voice." + +uv run scripts/tts_liquidaudio.py \ + "Don't ask what you can do for your country. Ask what your country can do for you." \ + --system "$SYSTEM_PROMPT" \ + --output output/tts_liquidaudio.wav diff --git a/scripts/tts_onnx.sh b/scripts/tts_onnx.sh new file mode 100755 index 0000000..eff69cc --- /dev/null +++ b/scripts/tts_onnx.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +set -x +mkdir -p output + +SYSTEM_PROMPT="Perform TTS. Use the UK female voice." + +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode tts \ + --prompt "Don't ask what you can do for your country. Ask what your country can do for you." \ + --system "$SYSTEM_PROMPT" \ + --output output/tts_onnx.wav diff --git a/scripts/tts_onnx_fp16.sh b/scripts/tts_onnx_fp16.sh new file mode 100755 index 0000000..3ad266a --- /dev/null +++ b/scripts/tts_onnx_fp16.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode tts \ + --precision fp16 \ + --prompt "Don't ask what you can do for your country. Ask what your country can do for you." \ + --output output/tts_onnx_fp16.wav diff --git a/scripts/tts_onnx_q4.sh b/scripts/tts_onnx_q4.sh new file mode 100755 index 0000000..b2ddc06 --- /dev/null +++ b/scripts/tts_onnx_q4.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode tts \ + --precision q4 \ + --prompt "Don't ask what you can do for your country. Ask what your country can do for you." \ + --output output/tts_onnx_q4.wav diff --git a/scripts/tts_onnx_q8.sh b/scripts/tts_onnx_q8.sh new file mode 100755 index 0000000..e1af916 --- /dev/null +++ b/scripts/tts_onnx_q8.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +set -x +mkdir -p output +uv run lfm2-audio-infer exports/LFM2.5-Audio-1.5B-ONNX \ + --mode tts \ + --precision q8 \ + --prompt "Don't ask what you can do for your country. Ask what your country can do for you." \ + --output output/tts_onnx_q8.wav diff --git a/src/liquidonnx/builder_base.py b/src/liquidonnx/builder_base.py index e706fe5..f96516f 100644 --- a/src/liquidonnx/builder_base.py +++ b/src/liquidonnx/builder_base.py @@ -184,6 +184,10 @@ def make_add(self, a: str, b: str, output_name: str) -> str: """Create Add node: output = a + b.""" return self.make_node("Add", [a, b], [output_name]) + def make_sub(self, a: str, b: str, output_name: str) -> str: + """Create Sub node: output = a - b.""" + return self.make_node("Sub", [a, b], [output_name]) + def make_mul(self, a: str, b: str, output_name: str) -> str: """Create Mul node: output = a * b.""" return self.make_node("Mul", [a, b], [output_name]) @@ -334,6 +338,151 @@ def make_linear( return matmul_out + def build_swiglu_ffn( + self, + input_name: str, + w1_name: str, + w2_name: str, + w3_name: str, + prefix: str, + residual: str | None = None, + ) -> str: + """Build SwiGLU feed-forward network. + + Architecture: w1 → w3 → silu(w1) * w3 → w2 [→ + residual] + + Args: + input_name: Input tensor (already normalized) + w1_name: Gate projection weight name (already transposed for MatMul) + w2_name: Down projection weight name (already transposed for MatMul) + w3_name: Up projection weight name (already transposed for MatMul) + prefix: Path prefix for node names + residual: Optional residual tensor to add at the end + + Returns: + Output tensor name + """ + # w1 (gate) and w3 (up) projections + w1_out = self.make_matmul(input_name, w1_name, f"{prefix}/w1/output_0") + w3_out = self.make_matmul(input_name, w3_name, f"{prefix}/w3/output_0") + + # SiLU(w1) * w3 + silu_out = self.make_silu(w1_out, f"{prefix}/silu") + gate_out = self.make_mul(silu_out, w3_out, f"{prefix}/gate/output_0") + + # w2 (down) projection + w2_out = self.make_matmul(gate_out, w2_name, f"{prefix}/w2/output_0") + + # Optional residual + if residual is not None: + return self.make_add(w2_out, residual, f"{prefix}/residual/output_0") + return w2_out + + def compute_rope_inv_freq(self, dim: int, theta: float = 10000.0) -> np.ndarray: + """Compute RoPE inverse frequencies. + + Args: + dim: Head dimension + theta: RoPE base frequency (default 10000.0) + + Returns: + Inverse frequencies array [dim//2] for RoPE computation + """ + return 1.0 / (theta ** (np.arange(0, dim, 2, dtype=np.float32) / dim)) + + def expand_kv_for_gqa( + self, + k: str, + v: str, + num_q_heads: int, + num_kv_heads: int, + head_dim: int, + prefix: str, + ) -> tuple[str, str]: + """Expand KV heads for grouped query attention. + + Expands KV tensors from [B, H_kv, T, D] to [B, H_q, T, D] by repeating + each KV head (num_q_heads // num_kv_heads) times. + + Args: + k: Key tensor name [B, num_kv_heads, T, head_dim] + v: Value tensor name [B, num_kv_heads, T, head_dim] + num_q_heads: Number of query heads + num_kv_heads: Number of KV heads + head_dim: Head dimension + prefix: Path prefix for node names + + Returns: + (expanded_k, expanded_v) tensor names, both [B, num_q_heads, T, head_dim] + """ + if num_q_heads == num_kv_heads: + return k, v + + num_groups = num_q_heads // num_kv_heads + + # Expand K: [B, H_kv, T, D] → [B, H_kv, 1, T, D] → [B, H_kv, G, T, D] → [B, H_q, T, D] + k_exp = self.make_unsqueeze(k, self.get_constant([2]), f"{prefix}/k_exp/output_0") + k_expanded = self.make_node( + "Expand", + [k_exp, self.get_constant([1, 1, num_groups, 1, 1])], + [f"{prefix}/k_expanded/output_0"], + ) + k_gqa = self.make_reshape( + k_expanded, + self.get_constant([0, num_q_heads, -1, head_dim]), + f"{prefix}/k_gqa/output_0", + ) + + # Expand V: same pattern + v_exp = self.make_unsqueeze(v, self.get_constant([2]), f"{prefix}/v_exp/output_0") + v_expanded = self.make_node( + "Expand", + [v_exp, self.get_constant([1, 1, num_groups, 1, 1])], + [f"{prefix}/v_expanded/output_0"], + ) + v_gqa = self.make_reshape( + v_expanded, + self.get_constant([0, num_q_heads, -1, head_dim]), + f"{prefix}/v_gqa/output_0", + ) + + return k_gqa, v_gqa + + def make_per_head_layernorm( + self, + tensor: str, + weight_name: str, + head_dim: int, + output_shape: list[int], + prefix: str, + epsilon: float = 1e-5, + ) -> str: + """Apply LayerNorm per-head by flattening, normalizing, and reshaping. + + Used for Q/K per-head normalization in attention layers. + + Args: + tensor: Input tensor name (any shape with last dim = head_dim) + weight_name: LayerNorm weight initializer name + head_dim: Head dimension (last dim size) + output_shape: Shape to reshape back to after normalization + prefix: Path prefix for node names + epsilon: LayerNorm epsilon + + Returns: + Normalized tensor reshaped to output_shape + """ + # Flatten to [-1, head_dim] + flat = self.make_reshape( + tensor, self.get_constant([-1, head_dim]), f"{prefix}/flat/output_0" + ) + + # Apply SimplifiedLayerNormalization (RMSNorm) + normed = self.make_layernorm(flat, weight_name, None, prefix, epsilon=epsilon) + + # Reshape back to output_shape + return self.make_reshape(normed, self.get_constant(output_shape), f"{prefix}/output_0") + def make_slice_last_n(self, input_name: str, n_elements: str, path: str, axis: int = 2) -> str: """Slice last N elements along axis (dynamic N). diff --git a/src/liquidonnx/lfm2/benchmark.py b/src/liquidonnx/lfm2/benchmark.py index 1621122..70d62ef 100644 --- a/src/liquidonnx/lfm2/benchmark.py +++ b/src/liquidonnx/lfm2/benchmark.py @@ -195,7 +195,7 @@ def main(): parser.add_argument("--max-tokens", type=int, default=20, help="Max tokens to generate") args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) benchmark = ONNXBenchmark(args.model) result = benchmark.benchmark(args.onnx, args.prompt, args.max_tokens) diff --git a/src/liquidonnx/lfm2/export.py b/src/liquidonnx/lfm2/export.py index 9cd2866..815b8f4 100644 --- a/src/liquidonnx/lfm2/export.py +++ b/src/liquidonnx/lfm2/export.py @@ -319,7 +319,7 @@ def main(): args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) # Derive output name from model path model_name = get_model_name(args.model) diff --git a/src/liquidonnx/lfm2/infer.py b/src/liquidonnx/lfm2/infer.py index 5afc967..c6e7ea3 100644 --- a/src/liquidonnx/lfm2/infer.py +++ b/src/liquidonnx/lfm2/infer.py @@ -29,7 +29,7 @@ def main(): parser.add_argument("--cpu", action="store_true", help="Force CPU execution (skip CUDA)") args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) model = ONNXTextModel(args.model, force_cpu=args.cpu) model.load() diff --git a/src/liquidonnx/lfm2_audio/__init__.py b/src/liquidonnx/lfm2_audio/__init__.py new file mode 100644 index 0000000..ef71945 --- /dev/null +++ b/src/liquidonnx/lfm2_audio/__init__.py @@ -0,0 +1 @@ +# LFM2.5-Audio ONNX export tools diff --git a/src/liquidonnx/lfm2_audio/builder/__init__.py b/src/liquidonnx/lfm2_audio/builder/__init__.py new file mode 100644 index 0000000..5bffdc8 --- /dev/null +++ b/src/liquidonnx/lfm2_audio/builder/__init__.py @@ -0,0 +1 @@ +# LFM2.5-Audio ONNX builders diff --git a/src/liquidonnx/lfm2_audio/builder/config.py b/src/liquidonnx/lfm2_audio/builder/config.py new file mode 100644 index 0000000..1de81c9 --- /dev/null +++ b/src/liquidonnx/lfm2_audio/builder/config.py @@ -0,0 +1,165 @@ +""" +Configuration dataclasses for LFM2.5-Audio ONNX export. + +Architecture Overview: + Audio waveform → Mel-spectrogram → Conformer → Adapter → LFM2 → Logits + ↓ + Depthformer → Audio codes + ↓ + Detokenizer → Waveform +""" + +from dataclasses import dataclass, field + + +@dataclass +class ConformerConfig: + """FastConformer encoder configuration.""" + + feat_in: int = 128 # Input mel features + d_model: int = 512 # Model dimension + n_layers: int = 17 # Number of conformer layers + n_heads: int = 8 # Attention heads + ff_expansion_factor: int = 4 # Feed-forward expansion + conv_kernel_size: int = 9 # Depthwise conv kernel + subsampling_factor: int = 8 # Temporal subsampling + subsampling_conv_channels: int = 256 # Subsampling conv channels + pos_emb_max_len: int = 5000 # Max position embeddings + + @classmethod + def from_hf_config(cls, encoder_config: dict) -> "ConformerConfig": + return cls( + feat_in=encoder_config.get("feat_in", 128), + d_model=encoder_config.get("d_model", 512), + n_layers=encoder_config.get("n_layers", 17), + n_heads=encoder_config.get("n_heads", 8), + ff_expansion_factor=encoder_config.get("ff_expansion_factor", 4), + conv_kernel_size=encoder_config.get("conv_kernel_size", 9), + subsampling_factor=encoder_config.get("subsampling_factor", 8), + subsampling_conv_channels=encoder_config.get("subsampling_conv_channels", 256), + pos_emb_max_len=encoder_config.get("pos_emb_max_len", 5000), + ) + + +@dataclass +class LFM2Config: + """LFM2 backbone configuration (same as text model).""" + + hidden_size: int = 2048 + num_hidden_layers: int = 16 + num_attention_heads: int = 32 + num_key_value_heads: int = 8 + vocab_size: int = 65536 + layer_types: list[str] = field(default_factory=list) + intermediate_size: int | None = None + conv_L_cache: int = 3 + max_position_embeddings: int = 128000 + norm_eps: float = 1e-5 + rope_theta: float = 1000000.0 + + def __post_init__(self): + if self.intermediate_size is None: + self.intermediate_size = self.hidden_size * 9 // 2 + + @classmethod + def from_hf_config(cls, lfm_config: dict) -> "LFM2Config": + intermediate_size = lfm_config.get("intermediate_size") + if intermediate_size is not None and lfm_config.get("block_auto_adjust_ff_dim", False): + intermediate_size = int(2 * intermediate_size / 3) + multiplier = lfm_config.get("block_ffn_dim_multiplier") + if multiplier is not None: + intermediate_size = int(multiplier * intermediate_size) + multiple_of = lfm_config.get("block_multiple_of", 256) + intermediate_size = multiple_of * ( + (intermediate_size + multiple_of - 1) // multiple_of + ) + + return cls( + hidden_size=lfm_config.get("hidden_size", 2048), + num_hidden_layers=lfm_config.get("num_hidden_layers", 16), + num_attention_heads=lfm_config.get("num_attention_heads", 32), + num_key_value_heads=lfm_config.get("num_key_value_heads", 8), + vocab_size=lfm_config.get("vocab_size", 65536), + layer_types=lfm_config.get("layer_types", []), + intermediate_size=intermediate_size, + conv_L_cache=lfm_config.get("conv_L_cache", 3), + max_position_embeddings=lfm_config.get("max_position_embeddings", 128000), + norm_eps=lfm_config.get("norm_eps", 1e-5), + rope_theta=lfm_config.get("rope_theta", 1000000.0), + ) + + +@dataclass +class DepthformerConfig: + """Depthformer configuration for audio codebook prediction.""" + + dim: int = 1024 + layers: int = 6 + n_heads: int = 32 # Derived from qkv_proj shape + head_dim: int = 32 # 1024 / 32 = 32 + intermediate_size: int = 2816 # From w1/w3 shape + n_codebooks: int = 8 + codebook_vocab_size: int = 2049 + + @classmethod + def from_hf_config(cls, df_config: dict) -> "DepthformerConfig": + return cls( + dim=df_config.get("dim", 1024), + layers=df_config.get("layers", 6), + ) + + +@dataclass +class DetokenizerConfig: + """Audio detokenizer configuration.""" + + hidden_size: int = 512 + num_hidden_layers: int = 8 + num_attention_heads: int = 16 + num_key_value_heads: int = 8 + layer_types: list[str] = field(default_factory=list) + intermediate_size: int = 3328 + conv_L_cache: int = 3 + sliding_window: int = 30 + output_size: int = 1282 # Magnitude + phase + vocab_size: int = 65536 + norm_eps: float = 1e-5 + rope_theta: float = 1000000.0 + + @classmethod + def from_hf_config(cls, config: dict) -> "DetokenizerConfig": + return cls( + hidden_size=config.get("hidden_size", 512), + num_hidden_layers=config.get("num_hidden_layers", 8), + num_attention_heads=config.get("num_attention_heads", 16), + num_key_value_heads=config.get("num_key_value_heads", 8), + layer_types=config.get("layer_types", []), + intermediate_size=config.get("intermediate_size", 3328), + conv_L_cache=config.get("conv_L_cache", 3), + sliding_window=config.get("sliding_window", 30), + output_size=config.get("output_size", 1282), + vocab_size=config.get("vocab_size", 65536), + norm_eps=config.get("norm_eps", 1e-5), + rope_theta=config.get("rope_theta", 1000000.0), + ) + + +@dataclass +class LFM2AudioConfig: + """Complete LFM2.5-Audio model configuration.""" + + conformer: ConformerConfig + lfm: LFM2Config + depthformer: DepthformerConfig + codebooks: int = 8 + audio_vocab_size: int = 16392 # 2049 * 8 + codebook_vocab_size: int = 2049 + + @classmethod + def from_hf_config(cls, config: dict) -> "LFM2AudioConfig": + return cls( + conformer=ConformerConfig.from_hf_config(config.get("encoder", {})), + lfm=LFM2Config.from_hf_config(config.get("lfm", {})), + depthformer=DepthformerConfig.from_hf_config(config.get("depthformer", {})), + codebooks=config.get("codebooks", 8), + ) diff --git a/src/liquidonnx/lfm2_audio/builder/conformer_builder.py b/src/liquidonnx/lfm2_audio/builder/conformer_builder.py new file mode 100644 index 0000000..c3d7b95 --- /dev/null +++ b/src/liquidonnx/lfm2_audio/builder/conformer_builder.py @@ -0,0 +1,1027 @@ +""" +FastConformer encoder builder for ONNX export. + +The FastConformer architecture processes mel-spectrograms through: +1. Subsampling: Reduces temporal resolution (factor 8) +2. Conformer blocks: Self-attention + convolution + feed-forward + +Each Conformer block has: + x → FFN1 (half) → MHA → Conv → FFN2 (half) → LayerNorm → out + with residual connections + +This implementation includes relative position attention matching +liquid-audio's RelPositionMultiHeadAttention: + - Uses linear_pos, pos_bias_u, pos_bias_v weights + - Computes content-content (matrix_ac) and content-position (matrix_bd) attention + - Applies rel_shift operation for positional alignment + - scores = (matrix_ac + matrix_bd) / sqrt(d_k) + +The adapter MLP is included at the end. +""" + +import logging + +import numpy as np +import onnx +from onnx import TensorProto, helper + +from liquidonnx.builder_base import ONNXBuilderBase + +from .config import ConformerConfig + +logger = logging.getLogger(__name__) + + +class ConformerEncoderBuilder(ONNXBuilderBase): + """Builds ONNX graph for FastConformer encoder + adapter.""" + + def __init__(self, config: ConformerConfig, adapter_output_dim: int = 2048): + super().__init__() + self.config = config + self.adapter_output_dim = adapter_output_dim + + def build_inputs(self): + self.inputs.append( + helper.make_tensor_value_info( + "mel_spectrogram", + TensorProto.FLOAT, + ["batch_size", "time_steps", self.config.feat_in], + ) + ) + self.inputs.append( + helper.make_tensor_value_info("mel_lengths", TensorProto.INT64, ["batch_size"]) + ) + + def build_outputs(self): + self.outputs.append( + helper.make_tensor_value_info( + "audio_embeddings", + TensorProto.FLOAT, + ["batch_size", "reduced_time", self.adapter_output_dim], + ) + ) + self.outputs.append( + helper.make_tensor_value_info("audio_lengths", TensorProto.INT64, ["batch_size"]) + ) + + def build_subsampling(self, input_name: str) -> str: + """Build subsampling layer (ConvSubsampling from liquid-audio). + + Subsampling reduces temporal resolution by factor of 8: + [B, T, 128] → [B, T//8, 512] + + Architecture: + [B, T, 128] → reshape [B, 1, T, 128] + → Conv2d(1→256, k=3, s=2) → ReLU # conv.0, conv.1 + → DepthwiseConv(256, k=3, s=2) → PointwiseConv(256) # conv.2, conv.3 + → DepthwiseConv(256, k=3, s=2) → PointwiseConv(256) # conv.5, conv.6 + → reshape [B, T//8, 256*F'] → Linear(d_model) # out + + Weight mapping: + conformer.pre_encode.conv.0 → Conv2d(1→256, groups=1, stride=2) + conformer.pre_encode.conv.2 → DepthwiseConv(groups=256, stride=2) + conformer.pre_encode.conv.3 → PointwiseConv (1x1) + conformer.pre_encode.conv.5 → DepthwiseConv(groups=256, stride=2) + conformer.pre_encode.conv.6 → PointwiseConv (1x1) + conformer.pre_encode.out → linear projection + """ + prefix = "/encoder/pre_encode" + C = self.config.subsampling_conv_channels # 256 + + # Reshape for Conv2d: [B, T, F] → [B, 1, T, F] + reshaped = self.make_unsqueeze( + input_name, self.get_constant([1]), f"{prefix}/Unsqueeze/output_0" + ) + + # === Block 1: Conv2d(1→256) + ReLU === + # conv.0 weight is [256, 1, 3, 3] - regular conv, not depthwise + conv0 = self.make_node( + "Conv", + [reshaped, "encoder.pre_encode.conv.0.weight", "encoder.pre_encode.conv.0.bias"], + [f"{prefix}/conv0/Conv/output_0"], + kernel_shape=[3, 3], + strides=[2, 2], + pads=[1, 1, 1, 1], + group=1, # Regular conv, not depthwise + ) + relu0 = self.make_node("Relu", [conv0], [f"{prefix}/conv0/Relu/output_0"]) + + # Compute valid length after conv0: (input_len + 2*pad - kernel) // stride + 1 + # = (mel_lengths + 2 - 3) // 2 + 1 = (mel_lengths - 1) // 2 + 1 + len_after_conv0 = self._compute_conv_length( + "mel_lengths", kernel=3, stride=2, pad=1, prefix=f"{prefix}/len0" + ) + + # === Block 2: Depthwise conv (stride 2) + Pointwise conv + ReLU === + conv2 = self.make_node( + "Conv", + [relu0, "encoder.pre_encode.conv.2.weight", "encoder.pre_encode.conv.2.bias"], + [f"{prefix}/conv2/Conv/output_0"], + kernel_shape=[3, 3], + strides=[2, 2], + pads=[1, 1, 1, 1], + group=C, + ) + conv3 = self.make_node( + "Conv", + [conv2, "encoder.pre_encode.conv.3.weight", "encoder.pre_encode.conv.3.bias"], + [f"{prefix}/conv3/Conv/output_0"], + kernel_shape=[1, 1], + strides=[1, 1], + ) + relu3 = self.make_node("Relu", [conv3], [f"{prefix}/conv3/Relu/output_0"]) + + # Compute valid length after conv2 and apply mask before conv5 + len_after_conv2 = self._compute_conv_length( + len_after_conv0, kernel=3, stride=2, pad=1, prefix=f"{prefix}/len2" + ) + # Apply mask to zero out positions >= len_after_conv2 + # relu3 shape: [B, C, T, F] + relu3_masked = self._apply_conv2d_mask(relu3, len_after_conv2, f"{prefix}/mask2") + + # === Block 3: Depthwise conv (stride 2) + Pointwise conv + ReLU === + conv5 = self.make_node( + "Conv", + [relu3_masked, "encoder.pre_encode.conv.5.weight", "encoder.pre_encode.conv.5.bias"], + [f"{prefix}/conv5/Conv/output_0"], + kernel_shape=[3, 3], + strides=[2, 2], + pads=[1, 1, 1, 1], + group=C, + ) + conv6 = self.make_node( + "Conv", + [conv5, "encoder.pre_encode.conv.6.weight", "encoder.pre_encode.conv.6.bias"], + [f"{prefix}/conv6/Conv/output_0"], + kernel_shape=[1, 1], + strides=[1, 1], + ) + relu6 = self.make_node("Relu", [conv6], [f"{prefix}/conv6/Relu/output_0"]) + + # Reshape: [B, C, T', F'] → [B, T', C*F'] + self.make_node("Shape", [relu6], [f"{prefix}/Shape/output_0"]) + batch = self.make_node( + "Gather", + [f"{prefix}/Shape/output_0", self.get_constant(0)], + [f"{prefix}/batch/output_0"], + axis=0, + ) + time = self.make_node( + "Gather", + [f"{prefix}/Shape/output_0", self.get_constant(2)], + [f"{prefix}/time/output_0"], + axis=0, + ) + + # Transpose: [B, C, T, F] → [B, T, C, F] + transposed = self.make_transpose(relu6, f"{prefix}/Transpose/output_0", perm=[0, 2, 1, 3]) + + # Flatten last two dims: [B, T, C*F] + new_shape = self.make_concat( + [ + self.make_unsqueeze(batch, self.get_constant([0]), f"{prefix}/batch_u/output_0"), + self.make_unsqueeze(time, self.get_constant([0]), f"{prefix}/time_u/output_0"), + self.get_constant([-1]), + ], + f"{prefix}/new_shape/output_0", + axis=0, + ) + flattened = self.make_reshape(transposed, new_shape, f"{prefix}/Reshape/output_0") + + # Linear projection to d_model + return self.make_linear( + flattened, + self.weights["conformer.pre_encode.out.weight"], + "encoder.pre_encode.out.weight", + f"{prefix}/out", + bias=self.weights["conformer.pre_encode.out.bias"], + bias_name="encoder.pre_encode.out.bias", + ) + + def build_conformer_block(self, layer_idx: int, hidden_state: str, pos_emb_name: str) -> str: + """Build a single Conformer block. + + Structure: + x → FFN1(half residual) → Self-Attn → Conv → FFN2(half residual) → LayerNorm + """ + prefix = f"/encoder/layers.{layer_idx}" + + # === Feed-forward 1 (half residual) === + ffn1_out = self.build_feed_forward(hidden_state, layer_idx, "feed_forward1") + # Half residual: x + 0.5 * ffn1_out + half_const = self.get_constant(0.5, dtype=np.float32) + ffn1_scaled = self.make_mul(ffn1_out, half_const, f"{prefix}/ffn1/Mul/output_0") + hidden_state = self.make_add(hidden_state, ffn1_scaled, f"{prefix}/ffn1/Add/output_0") + + # === Self-Attention with relative position encoding === + attn_out = self.build_self_attention(hidden_state, layer_idx, pos_emb_name) + hidden_state = self.make_add(hidden_state, attn_out, f"{prefix}/attn/Add/output_0") + + # === Convolution module === + conv_out = self.build_conv_module(hidden_state, layer_idx) + hidden_state = self.make_add(hidden_state, conv_out, f"{prefix}/conv/Add/output_0") + + # === Feed-forward 2 (half residual) === + ffn2_out = self.build_feed_forward(hidden_state, layer_idx, "feed_forward2") + ffn2_scaled = self.make_mul(ffn2_out, half_const, f"{prefix}/ffn2/Mul/output_0") + hidden_state = self.make_add(hidden_state, ffn2_scaled, f"{prefix}/ffn2/Add/output_0") + + # === Final LayerNorm === + return self.make_layernorm( + hidden_state, + f"encoder.layers.{layer_idx}.norm_out.weight", + f"encoder.layers.{layer_idx}.norm_out.bias", + f"{prefix}/norm_out", + ) + + def build_feed_forward(self, hidden_state: str, layer_idx: int, name: str) -> str: + """Build feed-forward module: LayerNorm → Linear → SiLU → Linear.""" + prefix = f"/encoder/layers.{layer_idx}/{name}" + weight_prefix = f"conformer.layers.{layer_idx}.{name}" + + # LayerNorm + normed = self.make_layernorm( + hidden_state, + f"encoder.layers.{layer_idx}.norm_{name}.weight", + f"encoder.layers.{layer_idx}.norm_{name}.bias", + f"{prefix}/norm", + ) + + # Linear 1 (expand) + linear1 = self.make_linear( + normed, + self.weights[f"{weight_prefix}.linear1.weight"], + f"encoder.layers.{layer_idx}.{name}.linear1.weight", + f"{prefix}/linear1", + bias=self.weights[f"{weight_prefix}.linear1.bias"], + bias_name=f"encoder.layers.{layer_idx}.{name}.linear1.bias", + ) + + # SiLU activation + silu = self.make_silu(linear1, f"{prefix}/act") + + # Linear 2 (project back) + return self.make_linear( + silu, + self.weights[f"{weight_prefix}.linear2.weight"], + f"encoder.layers.{layer_idx}.{name}.linear2.weight", + f"{prefix}/linear2", + bias=self.weights[f"{weight_prefix}.linear2.bias"], + bias_name=f"encoder.layers.{layer_idx}.{name}.linear2.bias", + ) + + def build_rel_positional_encoding(self, max_len: int = 5000) -> np.ndarray: + """Build sinusoidal relative positional encoding. + + For relative position encoding, we need positions from (L-1) to -(L-1), + totaling 2*L-1 positions for sequence length L. + + Returns: + pos_enc: [1, 2*max_len-1, d_model] sinusoidal encoding + """ + d_model = self.config.d_model + pe_len = 2 * max_len - 1 + + # Positions from (max_len-1) down to -(max_len-1) + positions = np.arange(max_len - 1, -max_len, -1, dtype=np.float32)[:, np.newaxis] + + # Div term: exp(i * -log(10000) / d_model) for i in 0, 2, 4, ... + div_term = np.exp(np.arange(0, d_model, 2, dtype=np.float32) * -(np.log(10000.0) / d_model)) + + pe = np.zeros((pe_len, d_model), dtype=np.float32) + pe[:, 0::2] = np.sin(positions * div_term) + pe[:, 1::2] = np.cos(positions * div_term) + + return pe[np.newaxis, :, :] # [1, 2*max_len-1, d_model] + + def build_rel_shift(self, x_name: str, prefix: str) -> str: + """Build relative shift operation for position encoding. + + The rel_shift operation transforms the raw position attention scores + to align positions correctly: + Input: [B, H, T, 2T-1] + 1. Pad left with zeros: [B, H, T, 2T] + 2. Reshape: [B, H, 2T, T] + 3. Drop first row: [B, H, 2T-1, T] + 4. Reshape back: [B, H, T, 2T-1] + + For ONNX we need to handle dynamic shapes. The sequence length T + is dynamic, so we build the graph to handle it. + """ + # Get shape: [B, H, T, pos_len] where pos_len = 2T-1 + self.make_node("Shape", [x_name], [f"{prefix}/shape/output_0"]) + + # Extract dimensions + batch = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(0), f"{prefix}/batch/output_0", axis=0 + ) + heads = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(1), f"{prefix}/heads/output_0", axis=0 + ) + qlen = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(2), f"{prefix}/qlen/output_0", axis=0 + ) + pos_len = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(3), f"{prefix}/pos_len/output_0", axis=0 + ) + + # Step 1: Pad left with zeros: [B, H, T, 2T-1] → [B, H, T, 2T] + # ONNX Pad format: [begin_d0, begin_d1, ..., begin_d(N-1), end_d0, end_d1, ..., end_d(N-1)] + # For padding dim 3 (pos_len) at the beginning by 1: pads = [0, 0, 0, 1, 0, 0, 0, 0] + pads = self.get_constant([0, 0, 0, 1, 0, 0, 0, 0]) + padded = self.make_node("Pad", [x_name, pads], [f"{prefix}/pad/output_0"], mode="constant") + + # Step 2: Reshape [B, H, T, pos_len+1] → [B, H, pos_len+1, T] + # new_pos_len = pos_len + 1 + new_pos_len = self.make_add(pos_len, self.get_constant(1), f"{prefix}/new_pos_len/output_0") + + reshape_shape = self.make_concat( + [ + self.make_unsqueeze(batch, self.get_constant([0]), f"{prefix}/b_u/output_0"), + self.make_unsqueeze(heads, self.get_constant([0]), f"{prefix}/h_u/output_0"), + self.make_unsqueeze(new_pos_len, self.get_constant([0]), f"{prefix}/p_u/output_0"), + self.make_unsqueeze(qlen, self.get_constant([0]), f"{prefix}/q_u/output_0"), + ], + f"{prefix}/reshape1_shape/output_0", + axis=0, + ) + reshaped = self.make_reshape(padded, reshape_shape, f"{prefix}/reshape1/output_0") + + # Step 3: Drop first row along dim 2 (pos_len+1 dim) + # Slice: starts=[0,0,1,0], ends=[max,max,max,max], axes=[0,1,2,3] + # Use dynamic slicing + starts = self.get_constant([0, 0, 1, 0]) + ends = self.make_concat( + [ + self.make_unsqueeze(batch, self.get_constant([0]), f"{prefix}/be/output_0"), + self.make_unsqueeze(heads, self.get_constant([0]), f"{prefix}/he/output_0"), + self.make_unsqueeze(new_pos_len, self.get_constant([0]), f"{prefix}/pe/output_0"), + self.make_unsqueeze(qlen, self.get_constant([0]), f"{prefix}/qe/output_0"), + ], + f"{prefix}/ends/output_0", + axis=0, + ) + axes = self.get_constant([0, 1, 2, 3]) + sliced = self.make_node( + "Slice", [reshaped, starts, ends, axes], [f"{prefix}/slice/output_0"] + ) + + # Step 4: Reshape back [B, H, pos_len, T] → [B, H, T, pos_len] + final_shape = self.make_concat( + [ + self.make_unsqueeze(batch, self.get_constant([0]), f"{prefix}/bf/output_0"), + self.make_unsqueeze(heads, self.get_constant([0]), f"{prefix}/hf/output_0"), + self.make_unsqueeze(qlen, self.get_constant([0]), f"{prefix}/qf/output_0"), + self.make_unsqueeze(pos_len, self.get_constant([0]), f"{prefix}/pf/output_0"), + ], + f"{prefix}/reshape2_shape/output_0", + axis=0, + ) + return self.make_reshape(sliced, final_shape, f"{prefix}/output_0") + + def build_self_attention(self, hidden_state: str, layer_idx: int, pos_emb_name: str) -> str: + """Build self-attention module with relative position encoding. + + Implements RelPositionMultiHeadAttention from liquid-audio: + q_with_bias_u = (q + pos_bias_u) + q_with_bias_v = (q + pos_bias_v) + matrix_ac = q_with_bias_u @ k.T (content-content) + matrix_bd = rel_shift(q_with_bias_v @ p.T) (content-position) + scores = (matrix_ac + matrix_bd) / sqrt(d_k) + """ + prefix = f"/encoder/layers.{layer_idx}/self_attn" + weight_prefix = f"conformer.layers.{layer_idx}.self_attn" + d_model = self.config.d_model + n_heads = self.config.n_heads + head_dim = d_model // n_heads + scale = 1.0 / (head_dim**0.5) + + # LayerNorm + normed = self.make_layernorm( + hidden_state, + f"encoder.layers.{layer_idx}.norm_self_att.weight", + f"encoder.layers.{layer_idx}.norm_self_att.bias", + f"{prefix}/norm", + ) + + # Q, K, V projections + q = self.make_linear( + normed, + self.weights[f"{weight_prefix}.linear_q.weight"], + f"encoder.layers.{layer_idx}.self_attn.q.weight", + f"{prefix}/q_proj", + bias=self.weights[f"{weight_prefix}.linear_q.bias"], + bias_name=f"encoder.layers.{layer_idx}.self_attn.q.bias", + ) + k = self.make_linear( + normed, + self.weights[f"{weight_prefix}.linear_k.weight"], + f"encoder.layers.{layer_idx}.self_attn.k.weight", + f"{prefix}/k_proj", + bias=self.weights[f"{weight_prefix}.linear_k.bias"], + bias_name=f"encoder.layers.{layer_idx}.self_attn.k.bias", + ) + v = self.make_linear( + normed, + self.weights[f"{weight_prefix}.linear_v.weight"], + f"encoder.layers.{layer_idx}.self_attn.v.weight", + f"{prefix}/v_proj", + bias=self.weights[f"{weight_prefix}.linear_v.bias"], + bias_name=f"encoder.layers.{layer_idx}.self_attn.v.bias", + ) + + # Project positional embeddings: [1, 2T-1, D] → [1, 2T-1, D] + p = self.make_linear( + pos_emb_name, + self.weights[f"{weight_prefix}.linear_pos.weight"], + f"encoder.layers.{layer_idx}.self_attn.linear_pos.weight", + f"{prefix}/pos_proj", + ) + + # Reshape for multi-head attention: [B, T, D] → [B, T, H, D/H] + reshape_const = self.get_constant([0, -1, n_heads, head_dim]) + q_4d = self.make_reshape(q, reshape_const, f"{prefix}/q_reshape/output_0") + k_4d = self.make_reshape(k, reshape_const, f"{prefix}/k_reshape/output_0") + v_4d = self.make_reshape(v, reshape_const, f"{prefix}/v_reshape/output_0") + p_4d = self.make_reshape(p, reshape_const, f"{prefix}/p_reshape/output_0") + + # Transpose K and V to [B, H, T, D/H] + k_t = self.make_transpose(k_4d, f"{prefix}/k_transpose/output_0", perm=[0, 2, 1, 3]) + v_t = self.make_transpose(v_4d, f"{prefix}/v_transpose/output_0", perm=[0, 2, 1, 3]) + # P to [B, H, 2T-1, D/H] (but B=1 for pos embeddings) + p_t = self.make_transpose(p_4d, f"{prefix}/p_transpose/output_0", perm=[0, 2, 1, 3]) + + # Add pos_bias_u and pos_bias_v to query + # q_4d is [B, T, H, D/H], pos_bias is [H, D/H] + # Need to broadcast: reshape pos_bias to [1, 1, H, D/H] + pos_bias_u = f"encoder.layers.{layer_idx}.self_attn.pos_bias_u" + pos_bias_v = f"encoder.layers.{layer_idx}.self_attn.pos_bias_v" + + # Unsqueeze pos_bias: [H, D/H] → [1, 1, H, D/H] + bias_u_unsq = self.make_unsqueeze( + pos_bias_u, self.get_constant([0, 1]), f"{prefix}/bias_u_unsq/output_0" + ) + bias_v_unsq = self.make_unsqueeze( + pos_bias_v, self.get_constant([0, 1]), f"{prefix}/bias_v_unsq/output_0" + ) + + # q + bias: [B, T, H, D/H] + [1, 1, H, D/H] → [B, T, H, D/H] + q_with_bias_u = self.make_add(q_4d, bias_u_unsq, f"{prefix}/q_bias_u/output_0") + q_with_bias_v = self.make_add(q_4d, bias_v_unsq, f"{prefix}/q_bias_v/output_0") + + # Transpose to [B, H, T, D/H] for matmul + q_u_t = self.make_transpose( + q_with_bias_u, f"{prefix}/q_u_transpose/output_0", perm=[0, 2, 1, 3] + ) + q_v_t = self.make_transpose( + q_with_bias_v, f"{prefix}/q_v_transpose/output_0", perm=[0, 2, 1, 3] + ) + + # === Content-content attention: matrix_ac = q_u @ k.T === + # [B, H, T, D/H] @ [B, H, D/H, T] → [B, H, T, T] + k_t_t = self.make_transpose(k_t, f"{prefix}/k_t_transpose/output_0", perm=[0, 1, 3, 2]) + matrix_ac = self.make_matmul(q_u_t, k_t_t, f"{prefix}/matrix_ac/output_0") + + # === Content-position attention: matrix_bd = rel_shift(q_v @ p.T) === + # [B, H, T, D/H] @ [1, H, D/H, 2T-1] → [B, H, T, 2T-1] + p_t_t = self.make_transpose(p_t, f"{prefix}/p_t_transpose/output_0", perm=[0, 1, 3, 2]) + matrix_bd_raw = self.make_matmul(q_v_t, p_t_t, f"{prefix}/matrix_bd_raw/output_0") + + # Apply rel_shift + matrix_bd_shifted = self.build_rel_shift(matrix_bd_raw, f"{prefix}/rel_shift") + + # Slice matrix_bd to match matrix_ac size (drop extra positions) + # matrix_bd_shifted is [B, H, T, 2T-1], need [B, H, T, T] + self.make_node("Shape", [matrix_ac], [f"{prefix}/ac_shape/output_0"]) + ac_last_dim = self.make_gather( + f"{prefix}/ac_shape/output_0", self.get_constant(3), f"{prefix}/ac_last/output_0" + ) + # Slice: [0:T] on last dimension + bd_starts = self.get_constant([0, 0, 0, 0]) + bd_ends = self.make_concat( + [ + self.get_constant([9223372036854775807]), # max int64 + self.get_constant([9223372036854775807]), + self.get_constant([9223372036854775807]), + self.make_unsqueeze( + ac_last_dim, self.get_constant([0]), f"{prefix}/ac_last_u/output_0" + ), + ], + f"{prefix}/bd_ends/output_0", + axis=0, + ) + bd_axes = self.get_constant([0, 1, 2, 3]) + matrix_bd = self.make_node( + "Slice", + [matrix_bd_shifted, bd_starts, bd_ends, bd_axes], + [f"{prefix}/matrix_bd/output_0"], + ) + + # === Combine: scores = (matrix_ac + matrix_bd) / sqrt(d_k) === + scores_sum = self.make_add(matrix_ac, matrix_bd, f"{prefix}/scores_sum/output_0") + scores = self.make_mul( + scores_sum, self.get_constant(scale, dtype=np.float32), f"{prefix}/scores/output_0" + ) + + # Softmax and attention + attn_weights = self.make_node("Softmax", [scores], [f"{prefix}/softmax/output_0"], axis=-1) + attn_out = self.make_matmul(attn_weights, v_t, f"{prefix}/attn_out/output_0") + + # Reshape back: [B, H, T, D/H] → [B, T, H, D/H] → [B, T, D] + attn_t = self.make_transpose( + attn_out, f"{prefix}/attn_transpose/output_0", perm=[0, 2, 1, 3] + ) + reshape_back = self.get_constant([0, -1, d_model]) + attn_flat = self.make_reshape(attn_t, reshape_back, f"{prefix}/attn_reshape/output_0") + + # Output projection + return self.make_linear( + attn_flat, + self.weights[f"{weight_prefix}.linear_out.weight"], + f"encoder.layers.{layer_idx}.self_attn.out.weight", + f"{prefix}/out_proj", + bias=self.weights[f"{weight_prefix}.linear_out.bias"], + bias_name=f"encoder.layers.{layer_idx}.self_attn.out.bias", + ) + + def build_conv_module(self, hidden_state: str, layer_idx: int) -> str: + """Build convolution module: LayerNorm → Conv1d (pointwise) → GLU → DepthConv → BN → SiLU → Conv1d.""" + prefix = f"/encoder/layers.{layer_idx}/conv" + + # LayerNorm + normed = self.make_layernorm( + hidden_state, + f"encoder.layers.{layer_idx}.norm_conv.weight", + f"encoder.layers.{layer_idx}.norm_conv.bias", + f"{prefix}/norm", + ) + + # Transpose for Conv1d: [B, T, C] → [B, C, T] + normed_t = self.make_transpose(normed, f"{prefix}/transpose1/output_0", perm=[0, 2, 1]) + + # Pointwise conv 1 (expand to 2*d_model for GLU) + pw1 = self.make_node( + "Conv", + [ + normed_t, + f"encoder.layers.{layer_idx}.conv.pointwise_conv1.weight", + f"encoder.layers.{layer_idx}.conv.pointwise_conv1.bias", + ], + [f"{prefix}/pw1/Conv/output_0"], + kernel_shape=[1], + ) + + # GLU: split in half, sigmoid one half, multiply + d_model = self.config.d_model + split_const = self.get_constant([d_model, d_model]) + self.make_node( + "Split", + [pw1, split_const], + [f"{prefix}/glu/Split/output_0", f"{prefix}/glu/Split/output_1"], + axis=1, + ) + glu_sigmoid = self.make_sigmoid( + f"{prefix}/glu/Split/output_1", f"{prefix}/glu/Sigmoid/output_0" + ) + glu_out = self.make_mul( + f"{prefix}/glu/Split/output_0", glu_sigmoid, f"{prefix}/glu/Mul/output_0" + ) + + # Depthwise conv + dw = self.make_node( + "Conv", + [ + glu_out, + f"encoder.layers.{layer_idx}.conv.depthwise_conv.weight", + f"encoder.layers.{layer_idx}.conv.depthwise_conv.bias", + ], + [f"{prefix}/dw/Conv/output_0"], + kernel_shape=[self.config.conv_kernel_size], + pads=[self.config.conv_kernel_size // 2, self.config.conv_kernel_size // 2], + group=d_model, + ) + + # Batch normalization (inference mode) + bn = self.make_node( + "BatchNormalization", + [ + dw, + f"encoder.layers.{layer_idx}.conv.batch_norm.weight", + f"encoder.layers.{layer_idx}.conv.batch_norm.bias", + f"encoder.layers.{layer_idx}.conv.batch_norm.running_mean", + f"encoder.layers.{layer_idx}.conv.batch_norm.running_var", + ], + [f"{prefix}/bn/BatchNormalization/output_0"], + epsilon=1e-5, + ) + + # SiLU + silu = self.make_silu(bn, f"{prefix}/act") + + # Pointwise conv 2 (project back) + pw2 = self.make_node( + "Conv", + [ + silu, + f"encoder.layers.{layer_idx}.conv.pointwise_conv2.weight", + f"encoder.layers.{layer_idx}.conv.pointwise_conv2.bias", + ], + [f"{prefix}/pw2/Conv/output_0"], + kernel_shape=[1], + ) + + # Transpose back: [B, C, T] → [B, T, C] + return self.make_transpose(pw2, f"{prefix}/transpose2/output_0", perm=[0, 2, 1]) + + def build_adapter(self, hidden_state: str) -> str: + """Build adapter MLP: LayerNorm → Linear → Linear.""" + prefix = "/encoder/adapter" + + # LayerNorm (from audio_adapter.model.0) + normed = self.make_layernorm( + hidden_state, + "encoder.adapter.norm.weight", + "encoder.adapter.norm.bias", + f"{prefix}/norm", + ) + + # Linear 1 + linear1 = self.make_linear( + normed, + self.weights["audio_adapter.model.1.weight"], + "encoder.adapter.linear1.weight", + f"{prefix}/linear1", + bias=self.weights["audio_adapter.model.1.bias"], + bias_name="encoder.adapter.linear1.bias", + ) + + # GELU activation (matching liquid-audio's GELU(approximate='none')) + gelu = self.make_node("Gelu", [linear1], [f"{prefix}/Gelu/output_0"]) + + # Linear 2 + return self.make_linear( + gelu, + self.weights["audio_adapter.model.3.weight"], + "encoder.adapter.linear2.weight", + f"{prefix}/linear2", + bias=self.weights["audio_adapter.model.3.bias"], + bias_name="encoder.adapter.linear2.bias", + ) + + def build_length_output(self) -> str: + """Compute output lengths after subsampling. + + The subsampling consists of 3 strided convolutions (stride=2 each). + For Conv2d with kernel=3, stride=2, pad=1: + output_len = (input_len + 2*pad - kernel) // stride + 1 + = (input_len + 2 - 3) // 2 + 1 + = (input_len - 1) // 2 + 1 + + Applied 3 times, this is NOT the same as input_len // 8. + """ + # Step 1: After conv0 (stride 2) + len_after_conv0 = self._compute_conv_length( + "mel_lengths", kernel=3, stride=2, pad=1, prefix="/encoder/len_out/conv0" + ) + # Step 2: After conv2 (stride 2) + len_after_conv2 = self._compute_conv_length( + len_after_conv0, kernel=3, stride=2, pad=1, prefix="/encoder/len_out/conv2" + ) + # Step 3: After conv5 (stride 2) + len_after_conv5 = self._compute_conv_length( + len_after_conv2, kernel=3, stride=2, pad=1, prefix="/encoder/len_out/conv5" + ) + # Cast back to int64 for output + return self.make_node( + "Cast", + [len_after_conv5], + ["audio_lengths"], + to=7, # int64 + name="/encoder/len_out/Cast", + ) + + def _compute_conv_length( + self, input_length: str, kernel: int, stride: int, pad: int, prefix: str + ) -> str: + """Compute output length after a strided convolution. + + Formula: (input_length + 2*pad - kernel) // stride + 1 + """ + # Cast to float for computation + length_float = self.make_node( + "Cast", + [input_length], + [f"{prefix}/cast/output_0"], + to=1, # float32 + ) + # (length + 2*pad - kernel) + numerator = self.make_node( + "Add", + [length_float, self.get_constant(2.0 * pad - kernel, dtype=np.float32)], + [f"{prefix}/numerator/output_0"], + ) + # // stride + divided = self.make_node( + "Div", + [numerator, self.get_constant(float(stride), dtype=np.float32)], + [f"{prefix}/divided/output_0"], + ) + floored = self.make_node("Floor", [divided], [f"{prefix}/floored/output_0"]) + # + 1 + result = self.make_node( + "Add", + [floored, self.get_constant(1.0, dtype=np.float32)], + [f"{prefix}/output_0"], + ) + return result + + def _apply_conv2d_mask(self, tensor: str, valid_length: str, prefix: str) -> str: + """Apply masking to zero out positions >= valid_length in time dimension. + + Args: + tensor: Input tensor of shape [B, C, T, F] + valid_length: Scalar or [B] tensor with valid lengths + prefix: Prefix for node names + + Returns: + Masked tensor of shape [B, C, T, F] + """ + # Get tensor shape + shape = self.make_node("Shape", [tensor], [f"{prefix}/shape/output_0"]) + # Get T (time dimension, index 2) + T = self.make_node( + "Gather", [shape, self.get_constant(2)], [f"{prefix}/T/output_0"], axis=0 + ) + + # Create range [0, 1, ..., T-1] + # Range needs start, limit, delta as scalars + zeros_scalar = self.get_constant(0, dtype=np.int64) + ones_scalar = self.get_constant(1, dtype=np.int64) + T_int = self.make_node("Cast", [T], [f"{prefix}/T_int/output_0"], to=7) # int64 + time_range = self.make_node( + "Range", [zeros_scalar, T_int, ones_scalar], [f"{prefix}/range/output_0"] + ) + + # Cast to float for comparison + time_range_float = self.make_node( + "Cast", + [time_range], + [f"{prefix}/range_float/output_0"], + to=1, # float32 + ) + + # valid_length might be [B], reshape to [B, 1] for broadcasting + # But for batch=1 case, it's just a scalar + # Create mask: time_range < valid_length → [T] bool mask + mask_bool = self.make_node( + "Less", [time_range_float, valid_length], [f"{prefix}/mask_bool/output_0"] + ) + + # Cast to float + mask_float = self.make_node( + "Cast", + [mask_bool], + [f"{prefix}/mask_float/output_0"], + to=1, # float32 + ) + + # Reshape mask from [T] to [1, 1, T, 1] for broadcasting with [B, C, T, F] + mask_reshaped = self.make_reshape( + mask_float, self.get_constant([1, 1, -1, 1]), f"{prefix}/mask_reshape/output_0" + ) + + # Apply mask + masked = self.make_mul(tensor, mask_reshaped, f"{prefix}/masked/output_0") + return masked + + def prepare_weights(self): + """Register all weights as initializers.""" + # Pre-encode (subsampling) weights - depthwise separable convolutions + # conv.0, conv.2, conv.5 are depthwise convs + # conv.3, conv.6 are pointwise convs + for idx in [0, 2, 3, 5, 6]: + w_name = f"conformer.pre_encode.conv.{idx}.weight" + b_name = f"conformer.pre_encode.conv.{idx}.bias" + if w_name in self.weights: + self.add_initializer(f"encoder.pre_encode.conv.{idx}.weight", self.weights[w_name]) + self.add_initializer(f"encoder.pre_encode.conv.{idx}.bias", self.weights[b_name]) + + # Linear projection + if "conformer.pre_encode.out.weight" in self.weights: + self.add_initializer( + "encoder.pre_encode.out.weight", + self.weights["conformer.pre_encode.out.weight"].T, + ) + self.add_initializer( + "encoder.pre_encode.out.bias", + self.weights["conformer.pre_encode.out.bias"], + ) + + # Conformer layer weights + for layer_idx in range(self.config.n_layers): + prefix = f"conformer.layers.{layer_idx}" + out_prefix = f"encoder.layers.{layer_idx}" + + # Layer norms + for norm_name in [ + "norm_feed_forward1", + "norm_feed_forward2", + "norm_self_att", + "norm_conv", + "norm_out", + ]: + w_name = f"{prefix}.{norm_name}.weight" + b_name = f"{prefix}.{norm_name}.bias" + if w_name in self.weights: + self.add_initializer(f"{out_prefix}.{norm_name}.weight", self.weights[w_name]) + self.add_initializer(f"{out_prefix}.{norm_name}.bias", self.weights[b_name]) + + # Feed-forward weights + for ff_name in ["feed_forward1", "feed_forward2"]: + for lin_name in ["linear1", "linear2"]: + w_name = f"{prefix}.{ff_name}.{lin_name}.weight" + b_name = f"{prefix}.{ff_name}.{lin_name}.bias" + if w_name in self.weights: + self.add_initializer( + f"{out_prefix}.{ff_name}.{lin_name}.weight", + self.weights[w_name].T, + ) + self.add_initializer( + f"{out_prefix}.{ff_name}.{lin_name}.bias", + self.weights[b_name], + ) + + # Attention weights (renamed for clarity) + for proj in ["q", "k", "v"]: + w_name = f"{prefix}.self_attn.linear_{proj}.weight" + b_name = f"{prefix}.self_attn.linear_{proj}.bias" + if w_name in self.weights: + self.add_initializer( + f"{out_prefix}.self_attn.{proj}.weight", self.weights[w_name].T + ) + self.add_initializer( + f"{out_prefix}.self_attn.{proj}.bias", self.weights[b_name] + ) + w_name = f"{prefix}.self_attn.linear_out.weight" + b_name = f"{prefix}.self_attn.linear_out.bias" + if w_name in self.weights: + self.add_initializer(f"{out_prefix}.self_attn.out.weight", self.weights[w_name].T) + self.add_initializer(f"{out_prefix}.self_attn.out.bias", self.weights[b_name]) + + # Relative position attention weights + pos_w = f"{prefix}.self_attn.linear_pos.weight" + if pos_w in self.weights: + self.add_initializer( + f"{out_prefix}.self_attn.linear_pos.weight", self.weights[pos_w].T + ) + pos_bias_u = f"{prefix}.self_attn.pos_bias_u" + if pos_bias_u in self.weights: + self.add_initializer(f"{out_prefix}.self_attn.pos_bias_u", self.weights[pos_bias_u]) + pos_bias_v = f"{prefix}.self_attn.pos_bias_v" + if pos_bias_v in self.weights: + self.add_initializer(f"{out_prefix}.self_attn.pos_bias_v", self.weights[pos_bias_v]) + + # Conv module weights + for conv_name in ["pointwise_conv1", "pointwise_conv2", "depthwise_conv"]: + w_name = f"{prefix}.conv.{conv_name}.weight" + b_name = f"{prefix}.conv.{conv_name}.bias" + if w_name in self.weights: + self.add_initializer( + f"{out_prefix}.conv.{conv_name}.weight", self.weights[w_name] + ) + self.add_initializer( + f"{out_prefix}.conv.{conv_name}.bias", self.weights[b_name] + ) + + # Batch norm + for bn_param in ["weight", "bias", "running_mean", "running_var"]: + name = f"{prefix}.conv.batch_norm.{bn_param}" + if name in self.weights: + self.add_initializer( + f"{out_prefix}.conv.batch_norm.{bn_param}", self.weights[name] + ) + + # Adapter weights + if "audio_adapter.model.0.weight" in self.weights: + self.add_initializer( + "encoder.adapter.norm.weight", self.weights["audio_adapter.model.0.weight"] + ) + self.add_initializer( + "encoder.adapter.norm.bias", self.weights["audio_adapter.model.0.bias"] + ) + + def load_weights(self, model_path: str): + """Load weights from HuggingFace model.""" + from huggingface_hub import hf_hub_download + from safetensors import safe_open + + logger.info(f"Loading conformer weights from {model_path}...") + + # Download safetensors file + safetensors_path = hf_hub_download(model_path, "model.safetensors") + + with safe_open(safetensors_path, framework="np", device="cpu") as f: + for key in f.keys(): + if key.startswith("conformer.") or key.startswith("audio_adapter."): + self.weights[key] = f.get_tensor(key) + + logger.info(f"Loaded {len(self.weights)} weights") + + def build_pos_encoding_slice(self, hidden_state: str, max_len: int = 5000) -> str: + """Build dynamic slicing of positional encoding based on sequence length. + + The full positional encoding is [1, 2*max_len-1, d_model]. + For input length T, we need positions [1, 2*T-1, d_model]. + + center_pos = (2*max_len-1) // 2 + 1 = max_len + start_pos = center_pos - T = max_len - T + end_pos = center_pos + T - 1 = max_len + T - 1 + """ + prefix = "/encoder/pos_enc" + + # Create full positional encoding as initializer + pe = self.build_rel_positional_encoding(max_len) + self.add_initializer("encoder.pos_enc", pe) + + # Get sequence length from hidden_state: [B, T, D] + self.make_node("Shape", [hidden_state], [f"{prefix}/shape/output_0"]) + seq_len = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(1), f"{prefix}/seq_len/output_0", axis=0 + ) + + # Compute slicing indices + # center_pos = max_len (constant) + center_pos = self.get_constant(max_len) + + # start_pos = center_pos - seq_len + start_pos = self.make_sub(center_pos, seq_len, f"{prefix}/start_pos/output_0") + + # end_pos = center_pos + seq_len - 1 + end_pos_tmp = self.make_add(center_pos, seq_len, f"{prefix}/end_pos_tmp/output_0") + end_pos = self.make_sub(end_pos_tmp, self.get_constant(1), f"{prefix}/end_pos/output_0") + + # Slice: pe[:, start_pos:end_pos, :] + starts = self.make_concat( + [ + self.get_constant([0]), + self.make_unsqueeze(start_pos, self.get_constant([0]), f"{prefix}/s_u/output_0"), + self.get_constant([0]), + ], + f"{prefix}/starts/output_0", + axis=0, + ) + ends = self.make_concat( + [ + self.get_constant([1]), + self.make_unsqueeze(end_pos, self.get_constant([0]), f"{prefix}/e_u/output_0"), + self.get_constant([self.config.d_model]), + ], + f"{prefix}/ends/output_0", + axis=0, + ) + axes = self.get_constant([0, 1, 2]) + + return self.make_node( + "Slice", ["encoder.pos_enc", starts, ends, axes], [f"{prefix}/output_0"] + ) + + def build(self, model_path: str) -> onnx.ModelProto: + """Build the complete ONNX model for audio encoder.""" + logger.info("Building Conformer encoder ONNX model...") + + # Load weights + self.load_weights(model_path) + + # Build graph structure + self.build_inputs() + self.build_outputs() + + # Prepare all weights as initializers + self.prepare_weights() + + # Build subsampling + hidden_state = self.build_subsampling("mel_spectrogram") + + # Note: xscale (sqrt(d_model)) is optional in RelPositionalEncoding + # LFM2.5-Audio has xscale=None, so we don't apply it + + # Build positional encoding (sliced based on sequence length) + pos_emb = self.build_pos_encoding_slice(hidden_state) + + # Build conformer layers + for layer_idx in range(self.config.n_layers): + logger.info(f"Building conformer layer {layer_idx}...") + hidden_state = self.build_conformer_block(layer_idx, hidden_state, pos_emb) + + # Build adapter (projects to LFM2 hidden size) + hidden_state = self.build_adapter(hidden_state) + + # Final output assignment + self.make_node("Identity", [hidden_state], ["audio_embeddings"], name="/encoder/output") + + # Build length output + self.build_length_output() + + model = self.build_graph("conformer_encoder") + logger.info(f"Model built: {len(self.nodes)} nodes") + return model diff --git a/src/liquidonnx/lfm2_audio/builder/depthformer_builder.py b/src/liquidonnx/lfm2_audio/builder/depthformer_builder.py new file mode 100644 index 0000000..98d1fbe --- /dev/null +++ b/src/liquidonnx/lfm2_audio/builder/depthformer_builder.py @@ -0,0 +1,767 @@ +""" +Depthformer ONNX builder for autoregressive audio codebook prediction. + +The depthformer predicts 8 audio codebook tokens autoregressively: +1. depth_linear: [B, 2048] → [B, 8, 1024] (integrated into this model) +2. depthformer transformer with KV cache (called 8× per frame) + +Architecture: + decoder hidden_states [B, 2048] + ↓ + depth_linear → [B, 8, 1024] (8 slices) + ↓ + depthformer_unified (8 iterations) → 8 tokens +""" + +import logging +import pathlib + +import numpy as np +import onnx +from onnx import TensorProto, helper + +from liquidonnx.builder_base import ONNXBuilderBase + +logger = logging.getLogger(__name__) + + +class DepthformerUnifiedBuilder(ONNXBuilderBase): + """Builder for vocoder_depthformer.onnx: autoregressive transformer with KV cache. + + Consolidates depth_linear projection, transformer step, 8 embedding tables, + and 8 logits projections into a single ONNX model. Uses step_idx input to + select appropriate weights. + + Architecture (per step): + 1. (First call only) Apply depth_linear: [B, 2048] → [B, 8, 1024] + 2. Gather current depth slice by step_idx + 3. Lookup prev_token embedding (zero for step 0) + 4. Add slice + embedding → transformer input [B, 1, 1024] + 5. 6 transformer layers with KV cache (GQA attention + SwiGLU FFN) + 6. Step-indexed RMSNorm and logits projection + + Inputs: + hidden_states: [B, 2048] - Decoder hidden states (depth_linear applied internally) + step_idx: scalar int64 - Which codebook step (0-7) + prev_token: [B] int64 - Previous step's sampled token + past_keys: [6, B, past_len, 8, 32] - KV cache keys + past_values: [6, B, past_len, 8, 32] - KV cache values + + Outputs: + logits: [B, 2049] - Codebook logits + depth_slices: [B, 8, 1024] - Depth slices (for subsequent steps) + new_keys: [6, B, new_len, 8, 32] - Updated KV cache keys + new_values: [6, B, new_len, 8, 32] - Updated KV cache values + """ + + def __init__(self): + super().__init__() + # Architecture constants + self.input_hidden_size = 2048 # From decoder + self.dim = 1024 # Depthformer hidden size + self.num_codebooks = 8 + self.num_layers = 6 + self.num_heads = 32 # Q heads + self.num_kv_heads = 8 # KV heads (GQA) + self.head_dim = 32 + self.intermediate_size = 2816 + self.vocab_size = 2049 + self.norm_eps = 1e-5 + self.rope_theta = 10000.0 + self.max_seq_len = 16 # Max positions for RoPE (8 steps + cache) + + def build_inputs(self): + """Build graph inputs.""" + # hidden_states: [B, 2048] - decoder output + self.inputs.append( + helper.make_tensor_value_info( + "hidden_states", TensorProto.FLOAT, ["batch", self.input_hidden_size] + ) + ) + # step_idx: scalar + self.inputs.append(helper.make_tensor_value_info("step_idx", TensorProto.INT64, [])) + # prev_token: [B] + self.inputs.append( + helper.make_tensor_value_info("prev_token", TensorProto.INT64, ["batch"]) + ) + # past_keys: [6, B, past_len, 8, 32] + self.inputs.append( + helper.make_tensor_value_info( + "past_keys", + TensorProto.FLOAT, + [self.num_layers, "batch", "past_len", self.num_kv_heads, self.head_dim], + ) + ) + # past_values: [6, B, past_len, 8, 32] + self.inputs.append( + helper.make_tensor_value_info( + "past_values", + TensorProto.FLOAT, + [self.num_layers, "batch", "past_len", self.num_kv_heads, self.head_dim], + ) + ) + + def build_outputs(self): + """Build graph outputs.""" + # logits: [B, 2049] + self.outputs.append( + helper.make_tensor_value_info("logits", TensorProto.FLOAT, ["batch", self.vocab_size]) + ) + # depth_slices: [B, 8, 1024] - output for reuse in subsequent steps + self.outputs.append( + helper.make_tensor_value_info( + "depth_slices", TensorProto.FLOAT, ["batch", self.num_codebooks, self.dim] + ) + ) + # new_keys: [6, B, new_len, 8, 32] + self.outputs.append( + helper.make_tensor_value_info( + "new_keys", + TensorProto.FLOAT, + [self.num_layers, "batch", "new_len", self.num_kv_heads, self.head_dim], + ) + ) + # new_values: [6, B, new_len, 8, 32] + self.outputs.append( + helper.make_tensor_value_info( + "new_values", + TensorProto.FLOAT, + [self.num_layers, "batch", "new_len", self.num_kv_heads, self.head_dim], + ) + ) + + def compute_rope_freqs(self) -> tuple[np.ndarray, np.ndarray]: + """Compute rotary position embedding frequencies. + + Returns: + freqs_cos: [max_seq_len, head_dim//2] + freqs_sin: [max_seq_len, head_dim//2] + """ + inv_freq = self.compute_rope_inv_freq(self.head_dim, self.rope_theta) + t = np.arange(self.max_seq_len) + freqs = np.outer(t, inv_freq) # [max_seq_len, head_dim//2] + freqs_cos = np.cos(freqs).astype(np.float32) + freqs_sin = np.sin(freqs).astype(np.float32) + return freqs_cos, freqs_sin + + def build_depth_linear(self) -> str: + """Build depth_linear projection: [B, 2048] → [B, 8, 1024]. + + Operations: + 1. MatMul: [B, 2048] × [2048, 8192] → [B, 8192] + 2. Add: [B, 8192] + [8192] → [B, 8192] + 3. Reshape: [B, 8192] → [B, 8, 1024] + + Returns: + Output tensor name for depth_slices [B, 8, 1024] + """ + prefix = "/depth_linear" + + # MatMul: hidden_states @ weight + matmul_out = self.make_matmul( + "hidden_states", "depth_linear.weight", f"{prefix}/MatMul/output_0" + ) + + # Add: + bias + add_out = self.make_add(matmul_out, "depth_linear.bias", f"{prefix}/Add/output_0") + + # Reshape: [B, 8192] → [B, 8, 1024] + return self.make_reshape( + add_out, + self.get_constant([-1, self.num_codebooks, self.dim]), + "depth_slices", + ) + + def build_get_current_slice(self, depth_slices: str) -> str: + """Build gather operation to get current depth slice. + + depth_slices: [B, 8, 1024], step_idx: scalar → [B, 1024] + """ + prefix = "/get_slice" + + # Get batch size from depth_slices shape + self.make_node("Shape", [depth_slices], [f"{prefix}/shape/output_0"]) + batch_size = self.make_gather( + f"{prefix}/shape/output_0", self.get_constant(0), f"{prefix}/batch/output_0" + ) + + # Expand step_idx for gather: scalar → [B, 1, 1024] + step_unsq1 = self.make_unsqueeze( + "step_idx", self.get_constant([0]), f"{prefix}/step_unsq1/output_0" + ) + step_unsq2 = self.make_unsqueeze( + step_unsq1, self.get_constant([0]), f"{prefix}/step_unsq2/output_0" + ) + step_unsq3 = self.make_unsqueeze( + step_unsq2, self.get_constant([0]), f"{prefix}/step_unsq3/output_0" + ) + + # Expand to [B, 1, 1024] + batch_unsq = self.make_unsqueeze( + batch_size, self.get_constant([0]), f"{prefix}/batch_unsq/output_0" + ) + expand_shape = self.make_concat( + [batch_unsq, self.get_constant([1]), self.get_constant([self.dim])], + f"{prefix}/expand_shape/output_0", + axis=0, + ) + step_expanded = self.make_node( + "Expand", [step_unsq3, expand_shape], [f"{prefix}/step_exp/output_0"] + ) + + # Gather from depth_slices along axis=1 + gathered = self.make_node( + "GatherElements", + [depth_slices, step_expanded], + [f"{prefix}/gather/output_0"], + axis=1, + ) + + # Squeeze dim 1: [B, 1, 1024] → [B, 1024] + return self.make_node( + "Squeeze", [gathered, self.get_constant([1])], [f"{prefix}/squeeze/output_0"] + ) + + def build_prev_embedding(self) -> str: + """Build previous token embedding lookup with step 0 handling. + + For step 0: returns zeros + For steps 1-7: looks up prev_token in embed_weights[step_idx-1] + """ + prefix = "/prev_embed" + + # Clamp step_idx-1 to [0, 7] + step_minus_1 = self.make_add( + "step_idx", self.get_constant(-1), f"{prefix}/step_m1/output_0" + ) + prev_step = self.make_node( + "Clip", + [step_minus_1, self.get_constant(0), self.get_constant(7)], + [f"{prefix}/prev_step/output_0"], + ) + + # Get embedding table for prev_step: stacked_embeds[prev_step] → [2049, 1024] + prev_embed_table = self.make_gather( + "stacked_embed_weights", prev_step, f"{prefix}/table/output_0", axis=0 + ) + + # Look up prev_token: [B] → [B, 1024] + prev_embed_raw = self.make_node( + "Gather", [prev_embed_table, "prev_token"], [f"{prefix}/lookup/output_0"], axis=0 + ) + + # Zero out for step 0: mask = (step_idx == 0) ? 0 : 1 + is_zero = self.make_node( + "Equal", ["step_idx", self.get_constant(0)], [f"{prefix}/is_zero/output_0"] + ) + is_zero_float = self.make_node( + "Cast", [is_zero], [f"{prefix}/is_zero_f/output_0"], to=TensorProto.FLOAT + ) + # mask = 1 - is_zero_float + one_minus = self.make_add( + self.get_constant(1.0, dtype=np.float32), + self.make_node("Neg", [is_zero_float], [f"{prefix}/neg_is_zero/output_0"]), + f"{prefix}/mask/output_0", + ) + # Unsqueeze mask for broadcast: scalar → [1, 1] + mask_unsq = self.make_unsqueeze( + one_minus, self.get_constant([0]), f"{prefix}/mask_unsq/output_0" + ) + + return self.make_mul(prev_embed_raw, mask_unsq, f"{prefix}/masked/output_0") + + def build_rotary_embedding( + self, q: str, k: str, layer_idx: int, past_len_name: str + ) -> tuple[str, str]: + """Build rotary position embedding for Q and K. + + Args: + q: Query tensor name [B, 1, num_heads, head_dim] + k: Key tensor name [B, 1, num_kv_heads, head_dim] + layer_idx: Layer index + past_len_name: Name of past_len tensor + + Returns: + (rotated_q, rotated_k) tensor names + """ + prefix = f"/layers.{layer_idx}/rope" + hd = self.head_dim + + # Slice freqs for current position: freqs[past_len:past_len+1] + pos_start = self.make_unsqueeze( + past_len_name, self.get_constant([0]), f"{prefix}/pos_start/output_0" + ) + pos_end = self.make_add(pos_start, self.get_constant([1]), f"{prefix}/pos_end/output_0") + freqs_cos = self.make_slice( + "rope_freqs_cos", + pos_start, + pos_end, + self.get_constant([0]), + f"{prefix}/cos_slice/output_0", + ) # [1, hd//2] + freqs_sin = self.make_slice( + "rope_freqs_sin", + pos_start, + pos_end, + self.get_constant([0]), + f"{prefix}/sin_slice/output_0", + ) # [1, hd//2] + + # Reshape Q/K for real/imag split: [B, 1, H, D] → [B, 1, H, D//2, 2] + q_reshaped = self.make_reshape( + q, self.get_constant([0, 1, -1, hd // 2, 2]), f"{prefix}/q_reshape/output_0" + ) + k_reshaped = self.make_reshape( + k, self.get_constant([0, 1, -1, hd // 2, 2]), f"{prefix}/k_reshape/output_0" + ) + + # Split real/imag + self.make_node( + "Split", + [q_reshaped, self.get_constant([1, 1])], + [f"{prefix}/q_real/output_0", f"{prefix}/q_imag/output_0"], + axis=-1, + ) + self.make_node( + "Split", + [k_reshaped, self.get_constant([1, 1])], + [f"{prefix}/k_real/output_0", f"{prefix}/k_imag/output_0"], + axis=-1, + ) + + # Squeeze last dim: [B, 1, H, D//2, 1] → [B, 1, H, D//2] + q_real = self.make_node( + "Squeeze", + [f"{prefix}/q_real/output_0", self.get_constant([-1])], + [f"{prefix}/q_real_sq/output_0"], + ) + q_imag = self.make_node( + "Squeeze", + [f"{prefix}/q_imag/output_0", self.get_constant([-1])], + [f"{prefix}/q_imag_sq/output_0"], + ) + k_real = self.make_node( + "Squeeze", + [f"{prefix}/k_real/output_0", self.get_constant([-1])], + [f"{prefix}/k_real_sq/output_0"], + ) + k_imag = self.make_node( + "Squeeze", + [f"{prefix}/k_imag/output_0", self.get_constant([-1])], + [f"{prefix}/k_imag_sq/output_0"], + ) + + # Broadcast freqs: [1, D//2] → [1, 1, 1, D//2] + cos = self.make_unsqueeze( + freqs_cos, self.get_constant([0, 2]), f"{prefix}/cos_bcast/output_0" + ) + sin = self.make_unsqueeze( + freqs_sin, self.get_constant([0, 2]), f"{prefix}/sin_bcast/output_0" + ) + + # Apply rotation: (a*cos - b*sin) + i*(a*sin + b*cos) + # Q + q_out_real_1 = self.make_mul(q_real, cos, f"{prefix}/q_rc/output_0") + q_out_real_2 = self.make_mul(q_imag, sin, f"{prefix}/q_is/output_0") + q_out_real = self.make_add( + q_out_real_1, + self.make_node("Neg", [q_out_real_2], [f"{prefix}/q_is_neg/output_0"]), + f"{prefix}/q_out_real/output_0", + ) + q_out_imag_1 = self.make_mul(q_real, sin, f"{prefix}/q_rs/output_0") + q_out_imag_2 = self.make_mul(q_imag, cos, f"{prefix}/q_ic/output_0") + q_out_imag = self.make_add(q_out_imag_1, q_out_imag_2, f"{prefix}/q_out_imag/output_0") + + # K + k_out_real_1 = self.make_mul(k_real, cos, f"{prefix}/k_rc/output_0") + k_out_real_2 = self.make_mul(k_imag, sin, f"{prefix}/k_is/output_0") + k_out_real = self.make_add( + k_out_real_1, + self.make_node("Neg", [k_out_real_2], [f"{prefix}/k_is_neg/output_0"]), + f"{prefix}/k_out_real/output_0", + ) + k_out_imag_1 = self.make_mul(k_real, sin, f"{prefix}/k_rs/output_0") + k_out_imag_2 = self.make_mul(k_imag, cos, f"{prefix}/k_ic/output_0") + k_out_imag = self.make_add(k_out_imag_1, k_out_imag_2, f"{prefix}/k_out_imag/output_0") + + # Stack and flatten: [B, 1, H, D//2] × 2 → [B, 1, H, D//2, 2] → [B, 1, H, D] + q_stacked = self.make_node( + "Concat", + [ + self.make_unsqueeze( + q_out_real, self.get_constant([-1]), f"{prefix}/q_real_unsq/output_0" + ), + self.make_unsqueeze( + q_out_imag, self.get_constant([-1]), f"{prefix}/q_imag_unsq/output_0" + ), + ], + [f"{prefix}/q_stack/output_0"], + axis=-1, + ) + k_stacked = self.make_node( + "Concat", + [ + self.make_unsqueeze( + k_out_real, self.get_constant([-1]), f"{prefix}/k_real_unsq/output_0" + ), + self.make_unsqueeze( + k_out_imag, self.get_constant([-1]), f"{prefix}/k_imag_unsq/output_0" + ), + ], + [f"{prefix}/k_stack/output_0"], + axis=-1, + ) + + q_out = self.make_reshape( + q_stacked, self.get_constant([0, 1, -1, hd]), f"{prefix}/q_out/output_0" + ) + k_out = self.make_reshape( + k_stacked, self.get_constant([0, 1, -1, hd]), f"{prefix}/k_out/output_0" + ) + + return q_out, k_out + + def build_transformer_layer( + self, x: str, layer_idx: int, past_k: str, past_v: str, past_len_name: str + ) -> tuple[str, str, str]: + """Build a single transformer layer. + + Args: + x: Input tensor [B, 1, 1024] + layer_idx: Layer index + past_k: Past keys [B, past_len, num_kv_heads, head_dim] + past_v: Past values [B, past_len, num_kv_heads, head_dim] + past_len_name: Name of past_len scalar tensor + + Returns: + (output, new_k, new_v) tensor names + """ + prefix = f"/layers.{layer_idx}" + nh = self.num_heads + nkv = self.num_kv_heads + hd = self.head_dim + + residual = x + + # === LayerNorm (SimplifiedLayerNormalization = RMSNorm) === + normed = self.make_layernorm( + x, f"layer.{layer_idx}.operator_norm.weight", None, f"{prefix}/op_norm" + ) + + # === QKV Projection === + qkv = self.make_linear( + normed, + self.weights[f"depthformer.layers.{layer_idx}.operator.qkv_proj.weight"], + f"layer.{layer_idx}.qkv.weight", + f"{prefix}/qkv", + ) + + # Split QKV + q_dim = nh * hd # 1024 + kv_dim = nkv * hd # 256 + self.make_node( + "Split", + [qkv, self.get_constant([q_dim, kv_dim, kv_dim])], + [f"{prefix}/q/output_0", f"{prefix}/k/output_0", f"{prefix}/v/output_0"], + axis=-1, + ) + + # Reshape to [B, 1, H, D] + q_4d = self.make_reshape( + f"{prefix}/q/output_0", self.get_constant([0, 1, nh, hd]), f"{prefix}/q_4d/output_0" + ) + k_4d = self.make_reshape( + f"{prefix}/k/output_0", self.get_constant([0, 1, nkv, hd]), f"{prefix}/k_4d/output_0" + ) + v_4d = self.make_reshape( + f"{prefix}/v/output_0", self.get_constant([0, 1, nkv, hd]), f"{prefix}/v_4d/output_0" + ) + + # === Q/K LayerNorm (per-head) === + q_4d = self.make_per_head_layernorm( + q_4d, f"layer.{layer_idx}.q_ln.weight", hd, [-1, 1, nh, hd], f"{prefix}/q_ln" + ) + k_4d = self.make_per_head_layernorm( + k_4d, f"layer.{layer_idx}.k_ln.weight", hd, [-1, 1, nkv, hd], f"{prefix}/k_ln" + ) + + # === Rotary Embeddings === + q_rope, k_rope = self.build_rotary_embedding(q_4d, k_4d, layer_idx, past_len_name) + + # === KV Cache Concat === + new_k = self.make_concat([past_k, k_rope], f"{prefix}/new_k/output_0", axis=1) + new_v = self.make_concat([past_v, v_4d], f"{prefix}/new_v/output_0", axis=1) + + # === Attention === + q_t = self.make_transpose(q_rope, f"{prefix}/q_t/output_0", perm=[0, 2, 1, 3]) + k_t = self.make_transpose(new_k, f"{prefix}/k_t/output_0", perm=[0, 2, 1, 3]) + v_t = self.make_transpose(new_v, f"{prefix}/v_t/output_0", perm=[0, 2, 1, 3]) + + # GQA: expand KV heads to match Q heads + k_gqa, v_gqa = self.expand_kv_for_gqa(k_t, v_t, nh, nkv, hd, prefix) + + # Scaled dot-product attention + k_gqa_t = self.make_transpose(k_gqa, f"{prefix}/k_gqa_t/output_0", perm=[0, 1, 3, 2]) + scores = self.make_matmul(q_t, k_gqa_t, f"{prefix}/scores/output_0") + + scale = 1.0 / np.sqrt(hd) + scaled = self.make_mul( + scores, self.get_constant(scale, dtype=np.float32), f"{prefix}/scaled/output_0" + ) + + attn_weights = self.make_node("Softmax", [scaled], [f"{prefix}/attn_w/output_0"], axis=-1) + attn_out = self.make_matmul(attn_weights, v_gqa, f"{prefix}/attn_out/output_0") + + attn_t = self.make_transpose(attn_out, f"{prefix}/attn_t/output_0", perm=[0, 2, 1, 3]) + attn_flat = self.make_reshape( + attn_t, self.get_constant([0, 1, -1]), f"{prefix}/attn_flat/output_0" + ) + + # === Output Projection + Residual === + out_proj = self.make_linear( + attn_flat, + self.weights[f"depthformer.layers.{layer_idx}.operator.out_proj.weight"], + f"layer.{layer_idx}.out.weight", + f"{prefix}/out_proj", + ) + h = self.make_add(out_proj, residual, f"{prefix}/res1/output_0") + + # === FFN (SwiGLU) using shared helper === + ffn_normed = self.make_layernorm( + h, f"layer.{layer_idx}.ffn_norm.weight", None, f"{prefix}/ffn_norm" + ) + + # Register FFN weights + w1_name = f"layer.{layer_idx}.w1.weight" + w2_name = f"layer.{layer_idx}.w2.weight" + w3_name = f"layer.{layer_idx}.w3.weight" + + self.add_initializer( + w1_name, + self.weights[f"depthformer.layers.{layer_idx}.feed_forward.w1.weight"] + .astype(np.float32) + .T, + ) + self.add_initializer( + w2_name, + self.weights[f"depthformer.layers.{layer_idx}.feed_forward.w2.weight"] + .astype(np.float32) + .T, + ) + self.add_initializer( + w3_name, + self.weights[f"depthformer.layers.{layer_idx}.feed_forward.w3.weight"] + .astype(np.float32) + .T, + ) + + ffn_out = self.build_swiglu_ffn(ffn_normed, w1_name, w2_name, w3_name, f"{prefix}/ffn") + + # Residual + output = self.make_add(ffn_out, h, f"{prefix}/res2/output_0") + + return output, new_k, new_v + + def build_step_logits(self, x: str) -> str: + """Build step-indexed RMSNorm + logits projection. + + Args: + x: Transformer output [B, 1024] + + Returns: + logits tensor name [B, 2049] + """ + prefix = "/logits" + + # Get step-specific weights + norm_weight = self.make_gather( + "stacked_logits_norm_weights", "step_idx", f"{prefix}/norm_w/output_0", axis=0 + ) + logits_weight = self.make_gather( + "stacked_logits_weights", "step_idx", f"{prefix}/logits_w/output_0", axis=0 + ) + + # RMSNorm + x_normed = self.make_node( + "SimplifiedLayerNormalization", + [x, norm_weight], + [f"{prefix}/rms_norm/output_0"], + epsilon=self.norm_eps, + ) + + # Linear projection + logits_w_t = self.make_transpose( + logits_weight, f"{prefix}/logits_w_t/output_0", perm=[1, 0] + ) + return self.make_matmul(x_normed, logits_w_t, "logits") + + def load_weights(self, model_path: str): + """Load all depthformer and depth_linear weights from HuggingFace model.""" + from huggingface_hub import hf_hub_download + from safetensors.torch import load_file + + logger.info(f"Loading depthformer weights from {model_path}...") + safetensors_path = hf_hub_download(model_path, "model.safetensors") + + # Use torch to handle bfloat16 + weights_torch = load_file(safetensors_path) + + for key, tensor in weights_torch.items(): + # Load depth_linear, depthformer, and depth_embeddings weights + if ( + key.startswith("depthformer.") + or key.startswith("depth_embeddings.") + or key.startswith("depth_linear.") + ): + self.weights[key] = tensor.float().numpy() + + logger.info(f"Loaded {len(self.weights)} weights") + + def prepare_weights(self): + """Register all weights as initializers.""" + # === Depth linear weights (new) === + weight = self.weights["depth_linear.weight"].astype(np.float32).T + bias = self.weights["depth_linear.bias"].astype(np.float32) + self.add_initializer("depth_linear.weight", weight) + self.add_initializer("depth_linear.bias", bias) + + # === Stacked embeddings: [8, 2049, 1024] === + embed_list = [] + logits_list = [] + logits_norm_list = [] + for i in range(self.num_codebooks): + embed_list.append(self.weights[f"depth_embeddings.{i}.embedding.weight"]) + logits_list.append(self.weights[f"depth_embeddings.{i}.to_logits.weight"]) + logits_norm_list.append(self.weights[f"depth_embeddings.{i}.embedding_norm.weight"]) + + self.add_initializer("stacked_embed_weights", np.stack(embed_list, axis=0)) + self.add_initializer("stacked_logits_weights", np.stack(logits_list, axis=0)) + self.add_initializer("stacked_logits_norm_weights", np.stack(logits_norm_list, axis=0)) + + # === RoPE frequencies === + freqs_cos, freqs_sin = self.compute_rope_freqs() + self.add_initializer("rope_freqs_cos", freqs_cos) + self.add_initializer("rope_freqs_sin", freqs_sin) + + # === Per-layer weights === + for i in range(self.num_layers): + prefix = f"depthformer.layers.{i}" + + # operator_norm (RMSNorm) + self.add_initializer( + f"layer.{i}.operator_norm.weight", self.weights[f"{prefix}.operator_norm.weight"] + ) + + # Q/K layernorm + self.add_initializer( + f"layer.{i}.q_ln.weight", + self.weights[f"{prefix}.operator.bounded_attention.q_layernorm.weight"], + ) + self.add_initializer( + f"layer.{i}.k_ln.weight", + self.weights[f"{prefix}.operator.bounded_attention.k_layernorm.weight"], + ) + + # FFN norm + self.add_initializer( + f"layer.{i}.ffn_norm.weight", self.weights[f"{prefix}.ffn_norm.weight"] + ) + + def build(self, model_path: str) -> onnx.ModelProto: + """Build the complete ONNX model for depthformer (with integrated depth_linear).""" + logger.info("Building vocoder_depthformer ONNX model (with integrated depth_linear)...") + + # Load weights + self.load_weights(model_path) + + # Build graph structure + self.build_inputs() + self.build_outputs() + + # Prepare initializers + self.prepare_weights() + + # === Build computation graph === + + # 1. Apply depth_linear: [B, 2048] → [B, 8, 1024] + depth_slices = self.build_depth_linear() + + # 2. Get current depth slice + current_slice = self.build_get_current_slice(depth_slices) + + # 3. Get previous token embedding + prev_embed = self.build_prev_embedding() + + # 4. Combine: (slice + embed) → [B, 1, 1024] + combined = self.make_add(current_slice, prev_embed, "/input/combined/output_0") + x = self.make_unsqueeze(combined, self.get_constant([1]), "/input/unsqueeze/output_0") + + # 5. Get past_len from past_keys shape + self.make_node("Shape", ["past_keys"], ["/past_len/shape/output_0"]) + past_len = self.make_gather( + "/past_len/shape/output_0", self.get_constant(2), "/past_len/output_0" + ) + + # 6. Transformer layers + new_keys_list = [] + new_values_list = [] + + for i in range(self.num_layers): + layer_past_k = self.make_gather( + "past_keys", self.get_constant(i), f"/layer_past_k_{i}/output_0", axis=0 + ) + layer_past_v = self.make_gather( + "past_values", self.get_constant(i), f"/layer_past_v_{i}/output_0", axis=0 + ) + + x, new_k, new_v = self.build_transformer_layer( + x, i, layer_past_k, layer_past_v, past_len + ) + + new_k_unsq = self.make_unsqueeze( + new_k, self.get_constant([0]), f"/new_k_{i}_unsq/output_0" + ) + new_v_unsq = self.make_unsqueeze( + new_v, self.get_constant([0]), f"/new_v_{i}_unsq/output_0" + ) + new_keys_list.append(new_k_unsq) + new_values_list.append(new_v_unsq) + + # Stack new KV caches + self.make_concat(new_keys_list, "new_keys", axis=0) + self.make_concat(new_values_list, "new_values", axis=0) + + # 7. Squeeze to [B, 1024] and build logits + output = self.make_node( + "Squeeze", [x, self.get_constant([1])], ["/output/squeeze/output_0"] + ) + self.build_step_logits(output) + + model = self.build_graph("vocoder_depthformer", opset_version=21) + logger.info(f"Model built: {len(self.nodes)} nodes") + return model + + +def export_vocoder_depthformer(model_path: str, onnx_dir: pathlib.Path) -> pathlib.Path: + """Export vocoder_depthformer.onnx. + + Single unified model for audio codebook prediction: + - Input: hidden_states [B, 2048] from decoder + - Internal: depth_linear projection + autoregressive transformer + - Output: logits [B, 2049] for sampling, plus KV cache + + Args: + model_path: HuggingFace model ID or local path + onnx_dir: Output directory for ONNX models + + Returns: + Path to exported vocoder_depthformer.onnx + """ + builder = DepthformerUnifiedBuilder() + model = builder.build(model_path) + + output_path = onnx_dir / "vocoder_depthformer.onnx" + onnx.save(model, str(output_path)) + + logger.info(f"vocoder_depthformer saved to {output_path}") + return output_path diff --git a/src/liquidonnx/lfm2_audio/builder/detokenizer_builder.py b/src/liquidonnx/lfm2_audio/builder/detokenizer_builder.py new file mode 100644 index 0000000..e7c121e --- /dev/null +++ b/src/liquidonnx/lfm2_audio/builder/detokenizer_builder.py @@ -0,0 +1,706 @@ +""" +Audio Detokenizer ONNX builder. + +Converts audio codes (from depthformer) to STFT features for waveform synthesis. +""" + +import logging +import pathlib + +import numpy as np +import onnx +from onnx import TensorProto, helper + +from liquidonnx.builder_base import ONNXBuilderBase + +logger = logging.getLogger(__name__) + + +class AudioDetokenizerBuilder(ONNXBuilderBase): + """Builder for audio detokenizer ONNX export. + + The audio detokenizer has the following architecture: + 1. FusedEmbedding: 8 codebooks (2048 vocab each) → [B, T, 512] + 2. LFM (8 layers): Mix of conv and sliding_attention layers + 3. Linear: [B, T, 512] → [B, T, 1282] (STFT space) + + Layer types: ["conv", "conv", "sliding_attention", "conv", + "sliding_attention", "conv", "sliding_attention", "conv"] + """ + + def __init__(self, config: dict, weights: dict[str, np.ndarray]): + super().__init__() + self.config = config + self.weights = weights + + # Model configuration + self.hidden_size = config.get("hidden_size", 512) + self.num_attention_heads = config.get("num_attention_heads", 16) + self.num_key_value_heads = config.get("num_key_value_heads", 8) + self.head_dim = self.hidden_size // self.num_attention_heads + self.intermediate_size = config.get("intermediate_size") or (self.hidden_size * 9 // 2) + self.output_size = config.get("output_size", 1282) + self.norm_eps = config.get("norm_eps", 1e-5) + self.num_codebooks = 8 + self.codebook_vocab = 2048 + self.conv_L_cache = 3 + + # Layer types: 4 conv, 4 sliding_attention + self.layer_types = config.get( + "layer_types", + [ + "conv", + "conv", + "sliding_attention", + "conv", + "sliding_attention", + "conv", + "sliding_attention", + "conv", + ], + ) + self.num_layers = len(self.layer_types) + self.sliding_window = config.get("sliding_window", 30) + + def build_inputs(self): + self.inputs.append( + helper.make_tensor_value_info( + "audio_codes", TensorProto.INT64, ["batch_size", self.num_codebooks, "time"] + ) + ) + + def build_outputs(self): + self.outputs.append( + helper.make_tensor_value_info( + "stft_features", + TensorProto.FLOAT, + ["batch_size", "time", self.output_size], + ) + ) + + def build_embedding(self) -> str: + """Build fused codebook embedding. + + Input: audio_codes [B, 8, T] + Output: embedded [B, T, 512] + """ + # Embedding weight: [16384, 512] (8 codebooks * 2048) + emb_weight = self.weights["emb.emb.weight"].astype(np.float32) + self.add_initializer("emb.weight", emb_weight) + + # Codebook offsets: [0, 2048, 4096, ...] + offsets = np.array( + [i * self.codebook_vocab for i in range(self.num_codebooks)], dtype=np.int64 + ).reshape(1, self.num_codebooks, 1) + self.add_initializer("codebook_offsets", offsets) + + # Add offsets to codes: [B, 8, T] + self.make_node("Add", ["audio_codes", "codebook_offsets"], ["/emb/offset_codes/output_0"]) + + # Transpose: [B, 8, T] -> [B, T, 8] + self.make_node( + "Transpose", + ["/emb/offset_codes/output_0"], + ["/emb/transposed/output_0"], + perm=[0, 2, 1], + ) + + # Get shape for reshape back + self.make_node("Shape", ["/emb/transposed/output_0"], ["/emb/shape/output_0"]) + + # Flatten for gather: [B, T, 8] -> [B*T*8] + self.make_reshape("/emb/transposed/output_0", self.get_constant([-1]), "/emb/flat/output_0") + + # Gather embeddings: [B*T*8, 512] + self.make_gather("emb.weight", "/emb/flat/output_0", "/emb/gathered/output_0") + + # Get batch and time dimensions + batch_dim = self.make_slice( + "/emb/shape/output_0", + self.get_constant([0]), + self.get_constant([1]), + self.get_constant([0]), + "/emb/batch_dim/output_0", + ) + time_dim = self.make_slice( + "/emb/shape/output_0", + self.get_constant([1]), + self.get_constant([2]), + self.get_constant([0]), + "/emb/time_dim/output_0", + ) + + # Build reshape shape [B, T, 8, 512] + reshape_shape = self.make_concat( + [batch_dim, time_dim, self.get_constant([8]), self.get_constant([self.hidden_size])], + "/emb/reshape_shape/output_0", + axis=0, + ) + + # Reshape: [B*T*8, 512] -> [B, T, 8, 512] + self.make_reshape("/emb/gathered/output_0", reshape_shape, "/emb/reshaped/output_0") + + # Mean across codebooks: [B, T, 8, 512] -> [B, T, 512] + self.make_node( + "ReduceMean", + ["/emb/reshaped/output_0", self.get_constant([2])], + ["/emb/summed/output_0"], + keepdims=0, + ) + + emb_output = "/emb/summed/output_0" + + # === 6x Upsampling === + # [B, T, H] → transpose → [B, H, T] → resize 6x → [B, H, 6T] → transpose → [B, 6T, H] + self.make_transpose(emb_output, "/emb/pre_upsample_t/output_0", perm=[0, 2, 1]) + + # Resize: [B, H, T] → [B, H, 6*T] + self.add_initializer("upsample_scales", np.array([1.0, 1.0, 6.0], dtype=np.float32)) + self.add_initializer("empty_roi", np.array([], dtype=np.float32)) + + node = helper.make_node( + "Resize", + ["/emb/pre_upsample_t/output_0", "empty_roi", "upsample_scales"], + ["/emb/upsampled/output_0"], + name="/emb/upsample", + mode="nearest", + coordinate_transformation_mode="asymmetric", + nearest_mode="floor", + ) + self.nodes.append(node) + + # Transpose back: [B, H, 6T] → [B, 6T, H] + return self.make_transpose( + "/emb/upsampled/output_0", "/emb/post_upsample_t/output_0", perm=[0, 2, 1] + ) + + def build_mlp(self, layer_idx: int, hidden_state: str) -> str: + """Build MLP block (SwiGLU activation) using shared helper.""" + prefix = f"/lfm/layers.{layer_idx}" + weight_prefix = f"lfm.layers.{layer_idx}" + + residual = hidden_state + + # FFN LayerNorm + self.add_initializer( + f"{weight_prefix}.ffn_norm.weight", + self.weights[f"{weight_prefix}.ffn_norm.weight"].astype(np.float32), + ) + normed = self.make_layernorm( + hidden_state, + f"{weight_prefix}.ffn_norm.weight", + None, + f"{prefix}/ffn_norm", + epsilon=self.norm_eps, + ) + + # Prepare weights (transposed for MatMul) + w1_name = f"{weight_prefix}.w1.weight" + w2_name = f"{weight_prefix}.w2.weight" + w3_name = f"{weight_prefix}.w3.weight" + + self.add_initializer( + w1_name, self.weights[f"{weight_prefix}.feed_forward.w1.weight"].astype(np.float32).T + ) + self.add_initializer( + w2_name, self.weights[f"{weight_prefix}.feed_forward.w2.weight"].astype(np.float32).T + ) + self.add_initializer( + w3_name, self.weights[f"{weight_prefix}.feed_forward.w3.weight"].astype(np.float32).T + ) + + # Use shared SwiGLU helper + return self.build_swiglu_ffn( + normed, w1_name, w2_name, w3_name, f"{prefix}/mlp", residual=residual + ) + + def build_conv_layer(self, layer_idx: int, hidden_state: str) -> str: + """Build a conv layer (short convolution with gating).""" + prefix = f"/lfm/layers.{layer_idx}" + weight_prefix = f"lfm.layers.{layer_idx}" + H = self.hidden_size + L = self.conv_L_cache # kernel size + + residual = hidden_state + + # Operator LayerNorm + self.add_initializer( + f"{weight_prefix}.operator_norm.weight", + self.weights[f"{weight_prefix}.operator_norm.weight"].astype(np.float32), + ) + normed = self.make_layernorm( + hidden_state, + f"{weight_prefix}.operator_norm.weight", + None, + f"{prefix}/operator_norm", + epsilon=self.norm_eps, + ) + + # In projection: [B, T, H] -> [B, T, 3H] + in_proj_w = self.weights[f"{weight_prefix}.conv.in_proj.weight"].astype(np.float32).T + self.add_initializer(f"{weight_prefix}.in_proj.weight", in_proj_w) + in_proj = self.make_matmul( + normed, f"{weight_prefix}.in_proj.weight", f"{prefix}/conv/in_proj/output_0" + ) + + # Transpose: [B, T, 3H] -> [B, 3H, T] + in_proj_t = self.make_transpose( + in_proj, f"{prefix}/conv/transpose1/output_0", perm=[0, 2, 1] + ) + + # Split into B, C, x (each [B, H, T]) + node = helper.make_node( + "Split", + [in_proj_t, self.get_constant([H, H, H])], + [ + f"{prefix}/conv/B/output_0", + f"{prefix}/conv/C/output_0", + f"{prefix}/conv/x/output_0", + ], + name=f"{prefix}/conv/split", + axis=1, + ) + self.nodes.append(node) + + # Bx = B * x (input gating, no sigmoid) + Bx = self.make_mul( + f"{prefix}/conv/B/output_0", f"{prefix}/conv/x/output_0", f"{prefix}/conv/Bx/output_0" + ) + + # Pad Bx for causal convolution: [B, H, T] -> [B, H, L-1 + T] + self.add_initializer("conv_pads", np.array([0, 0, L - 1, 0, 0, 0], dtype=np.int64)) + Bx_padded = self.make_node( + "Pad", [Bx, "conv_pads"], [f"{prefix}/conv/padded/output_0"], mode="constant" + ) + + # Depthwise Conv1D (kernel=L, groups=H) + conv_w = self.weights[f"{weight_prefix}.conv.conv.weight"].astype(np.float32) + self.add_initializer(f"{weight_prefix}.conv.weight", conv_w) + conv_out = self.make_node( + "Conv", + [Bx_padded, f"{weight_prefix}.conv.weight"], + [f"{prefix}/conv/conv1d/output_0"], + kernel_shape=[L], + group=H, + ) + + # Output gating: y = C * conv_out + y = self.make_mul(f"{prefix}/conv/C/output_0", conv_out, f"{prefix}/conv/y/output_0") + + # Transpose: [B, H, T] -> [B, T, H] + y_t = self.make_transpose(y, f"{prefix}/conv/transpose2/output_0", perm=[0, 2, 1]) + + # Out projection + out_proj_w = self.weights[f"{weight_prefix}.conv.out_proj.weight"].astype(np.float32).T + self.add_initializer(f"{weight_prefix}.out_proj.weight", out_proj_w) + out_proj = self.make_matmul( + y_t, f"{weight_prefix}.out_proj.weight", f"{prefix}/conv/out_proj/output_0" + ) + + # Residual + hidden_state = self.make_add(residual, out_proj, f"{prefix}/conv/residual/output_0") + + # MLP + return self.build_mlp(layer_idx, hidden_state) + + def build_rope(self, q_4d: str, k_4d: str, prefix: str) -> tuple[str, str]: + """Apply Rotary Position Embedding (RoPE) to Q and K. + + Input shapes: [B, T, nh, hd] or [B, T, nkv, hd] + Output shapes: same as input + + PyTorch's rotate_half splits first/second half: + - rotate_half([x0, ..., x15, x16, ..., x31]) = [-x16, ..., -x31, x0, ..., x15] + - cos/sin are concatenated: [c0, ..., c15, c0, ..., c15] + """ + hd = self.head_dim + rope_theta = self.config.get("rope_theta", 1000000.0) + + # Precompute inverse frequencies + inv_freq = self.compute_rope_inv_freq(hd, rope_theta) + self.add_initializer(f"{prefix}/rope_inv_freq", inv_freq) + + # Get sequence length from Q shape [B, T, nh, hd] + self.make_node("Shape", [q_4d], [f"{prefix}/q_shape/output_0"]) + seq_len = self.make_gather( + f"{prefix}/q_shape/output_0", self.get_constant(1), f"{prefix}/seq_len/output_0" + ) + + # Create position indices: [0, 1, ..., T-1] + self.add_initializer("range_start", np.array(0, dtype=np.int64)) + self.add_initializer("range_step", np.array(1, dtype=np.int64)) + positions = self.make_node( + "Range", ["range_start", seq_len, "range_step"], [f"{prefix}/positions/output_0"] + ) + + # Cast positions to float and reshape to [T, 1] + positions_f = self.make_node( + "Cast", [positions], [f"{prefix}/positions_f/output_0"], to=TensorProto.FLOAT + ) + positions_r = self.make_reshape( + positions_f, self.get_constant([-1, 1]), f"{prefix}/positions_r/output_0" + ) + + # Compute position * inv_freq: [T, 1] * [hd//2] -> [T, hd//2] + freqs = self.make_mul(positions_r, f"{prefix}/rope_inv_freq", f"{prefix}/freqs/output_0") + + # Compute cos and sin: [T, hd//2] + cos_half = self.make_node("Cos", [freqs], [f"{prefix}/cos_half/output_0"]) + sin_half = self.make_node("Sin", [freqs], [f"{prefix}/sin_half/output_0"]) + + # Concatenate to get [T, hd]: [c0, c1, ..., c15, c0, c1, ..., c15] + cos_hd = self.make_concat([cos_half, cos_half], f"{prefix}/cos_hd/output_0", axis=-1) + sin_hd = self.make_concat([sin_half, sin_half], f"{prefix}/sin_hd/output_0", axis=-1) + + # Reshape for broadcast: [T, hd] -> [1, T, 1, hd] for [B, T, nh, hd] + cos_bc = self.make_reshape( + cos_hd, self.get_constant([1, -1, 1, hd]), f"{prefix}/cos_bc/output_0" + ) + sin_bc = self.make_reshape( + sin_hd, self.get_constant([1, -1, 1, hd]), f"{prefix}/sin_bc/output_0" + ) + + # === rotate_half for Q === + half_hd = hd // 2 + q_first = f"{prefix}/q_first/output_0" + q_second = f"{prefix}/q_second/output_0" + node = helper.make_node( + "Split", + [q_4d, self.get_constant([half_hd, half_hd])], + [q_first, q_second], + name=f"{prefix}/q_split", + axis=-1, + ) + self.nodes.append(node) + + # rotate_half: [-second, first] + q_second_neg = self.make_node("Neg", [q_second], [f"{prefix}/q_second_neg/output_0"]) + q_rot_half = self.make_concat( + [q_second_neg, q_first], f"{prefix}/q_rot_half/output_0", axis=-1 + ) + + # Apply: q_rot = q * cos + rotate_half(q) * sin + q_cos = self.make_mul(q_4d, cos_bc, f"{prefix}/q_cos/output_0") + q_sin = self.make_mul(q_rot_half, sin_bc, f"{prefix}/q_sin/output_0") + q_rope = self.make_add(q_cos, q_sin, f"{prefix}/q_rope/output_0") + + # === rotate_half for K === + k_first = f"{prefix}/k_first/output_0" + k_second = f"{prefix}/k_second/output_0" + node = helper.make_node( + "Split", + [k_4d, self.get_constant([half_hd, half_hd])], + [k_first, k_second], + name=f"{prefix}/k_split", + axis=-1, + ) + self.nodes.append(node) + + k_second_neg = self.make_node("Neg", [k_second], [f"{prefix}/k_second_neg/output_0"]) + k_rot_half = self.make_concat( + [k_second_neg, k_first], f"{prefix}/k_rot_half/output_0", axis=-1 + ) + + k_cos = self.make_mul(k_4d, cos_bc, f"{prefix}/k_cos/output_0") + k_sin = self.make_mul(k_rot_half, sin_bc, f"{prefix}/k_sin/output_0") + k_rope = self.make_add(k_cos, k_sin, f"{prefix}/k_rope/output_0") + + return q_rope, k_rope + + def build_attention_layer(self, layer_idx: int, hidden_state: str) -> str: + """Build a sliding attention layer with causal sliding window mask and RoPE.""" + prefix = f"/lfm/layers.{layer_idx}" + weight_prefix = f"lfm.layers.{layer_idx}" + H = self.hidden_size + nh = self.num_attention_heads + nkv = self.num_key_value_heads + hd = self.head_dim + + residual = hidden_state + + # Operator LayerNorm + self.add_initializer( + f"{weight_prefix}.operator_norm.weight", + self.weights[f"{weight_prefix}.operator_norm.weight"].astype(np.float32), + ) + normed = self.make_layernorm( + hidden_state, + f"{weight_prefix}.operator_norm.weight", + None, + f"{prefix}/operator_norm", + epsilon=self.norm_eps, + ) + + # Q/K/V projections + q_w = self.weights[f"{weight_prefix}.self_attn.q_proj.weight"].astype(np.float32).T + k_w = self.weights[f"{weight_prefix}.self_attn.k_proj.weight"].astype(np.float32).T + v_w = self.weights[f"{weight_prefix}.self_attn.v_proj.weight"].astype(np.float32).T + + self.add_initializer(f"{weight_prefix}.q.weight", q_w) + self.add_initializer(f"{weight_prefix}.k.weight", k_w) + self.add_initializer(f"{weight_prefix}.v.weight", v_w) + + q = self.make_matmul(normed, f"{weight_prefix}.q.weight", f"{prefix}/attn/q/output_0") + k = self.make_matmul(normed, f"{weight_prefix}.k.weight", f"{prefix}/attn/k/output_0") + v = self.make_matmul(normed, f"{weight_prefix}.v.weight", f"{prefix}/attn/v/output_0") + + # Q/K LayerNorm (per-head) + # Note: Cannot use make_per_head_layernorm here because it flattens to [-1, hd] + # which loses batch dimension info needed for output shape [0, -1, nh, hd]. + q_ln_w = self.weights[f"{weight_prefix}.self_attn.q_layernorm.weight"].astype(np.float32) + k_ln_w = self.weights[f"{weight_prefix}.self_attn.k_layernorm.weight"].astype(np.float32) + self.add_initializer(f"{weight_prefix}.q_ln.weight", q_ln_w) + self.add_initializer(f"{weight_prefix}.k_ln.weight", k_ln_w) + + # Reshape Q for per-head norm: [B, T, H] -> [B, T*nh, hd] + q_reshaped = self.make_reshape( + q, self.get_constant([0, -1, hd]), f"{prefix}/attn/q_reshape1/output_0" + ) + q_normed = self.make_layernorm( + q_reshaped, + f"{weight_prefix}.q_ln.weight", + None, + f"{prefix}/attn/q_norm", + epsilon=self.norm_eps, + ) + q_4d = self.make_reshape( + q_normed, self.get_constant([0, -1, nh, hd]), f"{prefix}/attn/q_4d/output_0" + ) + + k_reshaped = self.make_reshape( + k, self.get_constant([0, -1, hd]), f"{prefix}/attn/k_reshape1/output_0" + ) + k_normed = self.make_layernorm( + k_reshaped, + f"{weight_prefix}.k_ln.weight", + None, + f"{prefix}/attn/k_norm", + epsilon=self.norm_eps, + ) + k_4d = self.make_reshape( + k_normed, self.get_constant([0, -1, nkv, hd]), f"{prefix}/attn/k_4d/output_0" + ) + + # Apply RoPE to Q and K (before transpose) + q_rope, k_rope = self.build_rope(q_4d, k_4d, f"{prefix}/rope") + + # Transpose: [B, T, nh, hd] -> [B, nh, T, hd] + q_4d_t = self.make_transpose(q_rope, f"{prefix}/attn/q_4d_t/output_0", perm=[0, 2, 1, 3]) + k_4d_t = self.make_transpose(k_rope, f"{prefix}/attn/k_4d_t/output_0", perm=[0, 2, 1, 3]) + + v_4d = self.make_reshape( + v, self.get_constant([0, -1, nkv, hd]), f"{prefix}/attn/v_4d/output_0" + ) + v_4d_t = self.make_transpose(v_4d, f"{prefix}/attn/v_4d_t/output_0", perm=[0, 2, 1, 3]) + + # Scaled dot product attention + scale = 1.0 / np.sqrt(hd) + + # GQA: expand KV heads to match Q heads + k_gqa, v_gqa = self.expand_kv_for_gqa(k_4d_t, v_4d_t, nh, nkv, hd, f"{prefix}/attn") + + # K transpose for Q @ K^T: [B, nh, T, hd] -> [B, nh, hd, T] + k_gqa_t = self.make_transpose(k_gqa, f"{prefix}/attn/k_gqa_t/output_0", perm=[0, 1, 3, 2]) + + # Attention scores: Q @ K^T [B, nh, T, T] + scores = self.make_matmul(q_4d_t, k_gqa_t, f"{prefix}/attn/scores/output_0") + scores_scaled = self.make_mul( + scores, + self.get_constant(scale, dtype=np.float32), + f"{prefix}/attn/scores_scaled/output_0", + ) + + # === Causal Sliding Window Mask === + self.make_node("Shape", [scores], [f"{prefix}/attn/scores_shape/output_0"]) + seq_len = self.make_gather( + f"{prefix}/attn/scores_shape/output_0", + self.get_constant(2), + f"{prefix}/attn/seq_len/output_0", + ) + + # Create position indices: [0, 1, 2, ..., T-1] + indices = self.make_node( + "Range", + ["range_start", seq_len, "range_step"], + [f"{prefix}/attn/indices/output_0"], + ) + + # Create row indices [T, 1] and col indices [1, T] + row_idx = self.make_unsqueeze( + indices, self.get_constant([1]), f"{prefix}/attn/row_idx/output_0" + ) + col_idx = self.make_unsqueeze( + indices, self.get_constant([0]), f"{prefix}/attn/col_idx/output_0" + ) + + # Distance matrix: d_idx = col_idx - row_idx [T, T] + d_idx = self.make_node("Sub", [col_idx, row_idx], [f"{prefix}/attn/d_idx/output_0"]) + + # Mask conditions + cond1 = self.make_node( + "LessOrEqual", [d_idx, self.get_constant(0)], [f"{prefix}/attn/cond1/output_0"] + ) + sw_neg = -self.sliding_window + cond2 = self.make_node( + "Greater", [d_idx, self.get_constant(sw_neg)], [f"{prefix}/attn/cond2/output_0"] + ) + + # Combined mask + valid_mask = self.make_node("And", [cond1, cond2], [f"{prefix}/attn/valid_mask/output_0"]) + invalid_mask = self.make_node("Not", [valid_mask], [f"{prefix}/attn/invalid_mask/output_0"]) + invalid_mask_f = self.make_node( + "Cast", [invalid_mask], [f"{prefix}/attn/invalid_mask_f/output_0"], to=TensorProto.FLOAT + ) + mask_bias = self.make_mul( + invalid_mask_f, + self.get_constant(-1e9, dtype=np.float32), + f"{prefix}/attn/mask_bias/output_0", + ) + + # Add mask bias to scores + scores_masked = self.make_add( + scores_scaled, mask_bias, f"{prefix}/attn/scores_masked/output_0" + ) + + # Softmax on masked scores + attn_weights = self.make_node( + "Softmax", [scores_masked], [f"{prefix}/attn/softmax/output_0"], axis=-1 + ) + + # Attention output: [B, nh, T, hd] + attn_out = self.make_matmul(attn_weights, v_gqa, f"{prefix}/attn/attn_out/output_0") + + # Reshape back: [B, nh, T, hd] -> [B, T, H] + attn_out_t = self.make_transpose( + attn_out, f"{prefix}/attn/attn_out_t/output_0", perm=[0, 2, 1, 3] + ) + attn_out_3d = self.make_reshape( + attn_out_t, self.get_constant([0, -1, H]), f"{prefix}/attn/attn_out_3d/output_0" + ) + + # Output projection + o_w = self.weights[f"{weight_prefix}.self_attn.out_proj.weight"].astype(np.float32).T + self.add_initializer(f"{weight_prefix}.o.weight", o_w) + o_proj = self.make_matmul( + attn_out_3d, f"{weight_prefix}.o.weight", f"{prefix}/attn/o_proj/output_0" + ) + + # Residual + hidden_state = self.make_add(residual, o_proj, f"{prefix}/attn/residual/output_0") + + # MLP + return self.build_mlp(layer_idx, hidden_state) + + def build_output_linear(self, hidden_state: str) -> str: + """Build final linear projection to STFT space.""" + # Final embedding norm + if "lfm.embedding_norm.weight" in self.weights: + self.add_initializer( + "lfm.embedding_norm.weight", + self.weights["lfm.embedding_norm.weight"].astype(np.float32), + ) + hidden_state = self.make_layernorm( + hidden_state, + "lfm.embedding_norm.weight", + None, + "/lfm/final_norm", + epsilon=self.norm_eps, + ) + + # Linear projection: [B, T, H] -> [B, T, output_size] + lin_w = self.weights["lin.weight"].astype(np.float32).T + lin_b = self.weights.get("lin.bias", np.zeros(self.output_size)).astype(np.float32) + self.add_initializer("lin.weight", lin_w) + self.add_initializer("lin.bias", lin_b) + + lin_out = self.make_matmul(hidden_state, "lin.weight", "/lin/matmul/output_0") + return self.make_add(lin_out, "lin.bias", "stft_features") + + def build(self) -> onnx.ModelProto: + """Build the complete audio detokenizer ONNX model.""" + # Build inputs/outputs + self.build_inputs() + self.build_outputs() + + # Build embedding + hidden_state = self.build_embedding() + + # Build LFM layers + for layer_idx in range(self.num_layers): + layer_type = self.layer_types[layer_idx] + if layer_type == "sliding_attention": + logger.info( + f"Building detokenizer layer {layer_idx} (attention with sliding window)..." + ) + hidden_state = self.build_attention_layer(layer_idx, hidden_state) + else: + logger.info(f"Building detokenizer layer {layer_idx} (conv)...") + hidden_state = self.build_conv_layer(layer_idx, hidden_state) + + # Build output linear + self.build_output_linear(hidden_state) + + # Use inherited build_graph method + return self.build_graph("audio_detokenizer", ms_domain=False) + + +def export_audio_detokenizer_builder(model_path: str, onnx_dir: pathlib.Path) -> pathlib.Path: + """Export audio detokenizer using ONNX builder with full LFM layers. + + The audio detokenizer converts audio codes to waveform: + 1. Embedding: [B, 8, T] -> [B, T, 512] (fused codebook embedding) + 2. LFM: [B, T, 512] -> [B, T, 512] (8-layer transformer with conv and attention) + 3. Linear: [B, T, 512] -> [B, T, 1282] (STFT space projection) + 4. ISTFT: [B, T, 1282] -> waveform (done in numpy/scipy) + + This exports steps 1-3 to ONNX. Step 4 is done in numpy. + """ + logger.info("Exporting audio_detokenizer.onnx (full LFM builder version)...") + + import json as json_module + + from liquid_audio.utils import get_model_dir + from safetensors.torch import load_file + + cache_dir = get_model_dir(model_path) + config_path = cache_dir / "audio_detokenizer" / "config.json" + + if not config_path.exists(): + logger.warning("Audio detokenizer not found in model, skipping export") + return None + + with open(config_path) as f: + detok_config = json_module.load(f) + + logger.info(f"Audio detokenizer config: {detok_config}") + + # Load weights directly from checkpoint + weights_path = cache_dir / "audio_detokenizer" / "model.safetensors" + checkpoint_weights = load_file(str(weights_path)) + + # Convert to numpy + detok_weights = {} + for name, param in checkpoint_weights.items(): + detok_weights[name] = param.float().cpu().numpy() + + logger.info(f"Loaded {len(detok_weights)} audio detokenizer weights from checkpoint") + + # Add ISTFT window (needed for inference) + import torch + + istft_window = torch.hann_window(1280).numpy() + detok_weights["istft.window"] = istft_window + + # Build the model using AudioDetokenizerBuilder + builder = AudioDetokenizerBuilder(detok_config, detok_weights) + model = builder.build() + + # Save the model + output_path = onnx_dir / "audio_detokenizer.onnx" + onnx.save_model(model, str(output_path)) + logger.info(f"audio_detokenizer saved to {output_path}") + + # Note: ISTFT window is generated at runtime via np.hanning() in infer.py + # This makes inference compatible with transformers.js which cannot load numpy files + + return output_path diff --git a/src/liquidonnx/lfm2_audio/export.py b/src/liquidonnx/lfm2_audio/export.py new file mode 100644 index 0000000..132b271 --- /dev/null +++ b/src/liquidonnx/lfm2_audio/export.py @@ -0,0 +1,753 @@ +#!/usr/bin/env python3 +""" +ONNX export for LFM2.5-Audio model supporting all 3 modes: +- ASR (Automatic Speech Recognition): Audio -> Text +- TTS (Text-to-Speech): Text -> Audio +- Interleaved: Mixed text and audio I/O + +Exports the following ONNX models: +1. decoder.onnx - LFM2 backbone with text embeddings (input_ids -> logits/hidden_states) +2. audio_encoder.onnx - Conformer encoder for ASR (mel-spectrogram -> audio embeddings) +3. audio_embedding.onnx - Audio code embeddings for TTS/interleaved +4. audio_detokenizer.onnx - Neural vocoder for TTS (codes -> STFT features) + +Note: Depthformer (audio codebook prediction) uses PyTorch at inference time for +autoregressive generation, which produces higher quality audio than parallel ONNX. + +Usage: + uv run lfm2-audio-export LiquidAI/LFM2.5-Audio-1.5B + uv run lfm2-audio-export LiquidAI/LFM2.5-Audio-1.5B --precision fp16 + uv run lfm2-audio-export LiquidAI/LFM2.5-Audio-1.5B --precision q4 +""" + +import argparse +import gc +import json +import logging +import pathlib +import shutil + +import numpy as np +import onnx +from onnx import TensorProto, helper + +from liquidonnx.external_data import split_external_data +from liquidonnx.lfm2.builder import LFM2Builder, LFM2Config +from liquidonnx.lfm2_audio.builder.config import ConformerConfig +from liquidonnx.lfm2_audio.builder.conformer_builder import ConformerEncoderBuilder +from liquidonnx.lfm2_audio.builder.depthformer_builder import ( + export_vocoder_depthformer, +) +from liquidonnx.lfm2_audio.builder.detokenizer_builder import ( + export_audio_detokenizer_builder, +) +from liquidonnx.quantize import get_model_size, get_total_model_size_mb, quantize_model + +logger = logging.getLogger(__name__) + + +def get_model_name(model_path: str) -> str: + if "/" in model_path: + return model_path.split("/")[-1] + return pathlib.Path(model_path).name + + +def load_audio_model_weights(model_path: str) -> dict[str, np.ndarray]: + from huggingface_hub import hf_hub_download + from safetensors import safe_open + + logger.info(f"Loading weights from {model_path}...") + safetensors_path = hf_hub_download(model_path, "model.safetensors") + + weights = {} + with safe_open(safetensors_path, framework="np", device="cpu") as f: + for key in f.keys(): + weights[key] = f.get_tensor(key) + + logger.info(f"Loaded {len(weights)} weights") + return weights + + +def load_audio_config(model_path: str) -> dict: + from huggingface_hub import hf_hub_download + + config_path = hf_hub_download(model_path, "config.json") + with open(config_path) as f: + return json.load(f) + + +# === 1. Audio Encoder Export (builder) === + + +def export_audio_encoder_builder( + model_path: str, config: dict, onnx_dir: pathlib.Path +) -> pathlib.Path: + """Export Conformer audio encoder to ONNX using ONNX builder (no torch.onnx.export). + + Args: + model_path: HuggingFace model ID or local path + config: Model configuration dict + onnx_dir: Output directory for ONNX models + + Returns: + Path to exported audio_encoder.onnx + """ + logger.info("Exporting audio_encoder.onnx (builder)...") + + # Create conformer config from model config + encoder_config = config.get("encoder", {}) + conformer_config = ConformerConfig.from_hf_config(encoder_config) + + # Get adapter output dimension from LFM config + adapter_output_dim = config.get("lfm", {}).get("hidden_size", 2048) + + # Build the model + builder = ConformerEncoderBuilder(conformer_config, adapter_output_dim) + model = builder.build(model_path) + + output_path = onnx_dir / "audio_encoder.onnx" + onnx.save(model, str(output_path)) + + logger.info(f"audio_encoder saved to {output_path}") + return output_path + + +# === 2. Audio Embedding Export (builder) === +# Note: embed_tokens is NOT exported separately - it's included in decoder.onnx +# and extracted at inference time for text embedding lookup. + + +def export_audio_embedding( + weights: dict[str, np.ndarray], config: dict, onnx_dir: pathlib.Path +) -> pathlib.Path: + """Export audio_embedding.onnx (audio token embedding lookup). + + Note: The embedding lookup does NOT apply normalization. + The embedding_norm (RMSNorm) is only used in get_logits() for the inverse + projection (embedding -> logits), not for the forward embedding lookup. + + Reference: liquid_audio/model/transformer.py SharedEmbedding.embed() + """ + logger.info("Exporting audio_embedding.onnx...") + + nodes = [] + hidden_size = config.get("lfm", {}).get("hidden_size", 2048) + + embed_weight = weights["audio_embedding.embedding.weight"].astype(np.float32) + + inputs = [ + helper.make_tensor_value_info( + "audio_codes", TensorProto.INT64, ["batch_size", "audio_length"] + ) + ] + outputs = [ + helper.make_tensor_value_info( + "audio_embeds", + TensorProto.FLOAT, + ["batch_size", "audio_length", hidden_size], + ) + ] + + initializers = [ + onnx.numpy_helper.from_array(embed_weight, "audio_embedding.weight"), + ] + + # Just do embedding lookup - no normalization + nodes.append( + helper.make_node( + "Gather", + ["audio_embedding.weight", "audio_codes"], + ["audio_embeds"], + axis=0, + ) + ) + + graph = helper.make_graph(nodes, "audio_embedding", inputs, outputs, initializers) + model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 21)], ir_version=10) + model.producer_name = "liquidonnx" + + output_path = onnx_dir / "audio_embedding.onnx" + onnx.save_model(model, str(output_path)) + logger.info(f"audio_embedding saved to {output_path}") + return output_path + + +# === 3b. Audio Embedding Binary Export === +# +# Export audio_embedding.weight as raw binary for direct lookup. +# This eliminates the ONNX model call overhead (352 calls per generation). + + +def export_audio_embedding_binary( + weights: dict[str, np.ndarray], config: dict, onnx_dir: pathlib.Path +) -> pathlib.Path: + """Export audio_embedding weight as raw binary. + + The audio embedding table has shape [num_codebooks * codebook_vocab, hidden_size] + where num_codebooks=8 and codebook_vocab=2049 (including end-of-audio token). + + Token index = codebook_idx * 2049 + code_value + + Saves as: + 1. audio_embedding.bin - raw float32 binary + 2. audio_embedding.json - metadata (num_codebooks, codebook_vocab, hidden_size) + + This enables direct numpy/JS indexing instead of ONNX model calls. + """ + embed_weight = weights["audio_embedding.embedding.weight"] # [16392, hidden_size] + vocab_size, hidden_size = embed_weight.shape + + # Validate expected shape + num_codebooks = 8 + codebook_vocab = 2049 + expected_vocab = num_codebooks * codebook_vocab + if vocab_size != expected_vocab: + logger.warning(f"audio_embedding vocab_size={vocab_size}, expected {expected_vocab}") + + logger.info(f"audio_embedding weight shape: {embed_weight.shape}") + + # Save as raw binary (float32, little-endian) + bin_path = onnx_dir / "audio_embedding.bin" + embed_weight.astype(np.float32).tofile(bin_path) + logger.info(f"audio_embedding.bin saved ({bin_path.stat().st_size / 1e6:.1f} MB)") + + # Save metadata + meta_path = onnx_dir / "audio_embedding.json" + with open(meta_path, "w") as f: + json.dump( + { + "vocab_size": int(vocab_size), + "hidden_size": int(hidden_size), + "num_codebooks": num_codebooks, + "codebook_vocab": codebook_vocab, + "dtype": "float32", + "byte_order": "little", + }, + f, + indent=2, + ) + logger.info("audio_embedding.json saved") + + return bin_path + + +# === 3c. Text Embedding Export === +# +# Why export embed_tokens separately? +# +# The embed_tokens.weight is already stored in decoder.onnx (for the tied LM head), +# but we export it as a standalone binary file for cross-platform consistency: +# +# - Python CAN extract weights from ONNX via `onnx.load()` + graph.initializer +# - JavaScript CANNOT - ONNX Runtime Web only exposes inference APIs, not internals +# +# By exporting as raw binary, both Python and JS use the same artifact and code path: +# weight = load_binary("embed_tokens.bin") +# embedding = weight[token_id] +# +# This avoids maintaining two different extraction methods and ensures identical behavior. + + +def export_embed_tokens( + weights: dict[str, np.ndarray], config: dict, onnx_dir: pathlib.Path +) -> pathlib.Path: + """Export embed_tokens weight as raw binary. + + Saves embed_tokens.weight as: + 1. embed_tokens.bin - raw float32 binary (vocab_size * hidden_size * 4 bytes) + 2. embed_tokens.json - metadata (vocab_size, hidden_size, dtype) + + Both Python and JavaScript load this the same way for consistency. + """ + embed_weight = weights["lfm.embed_tokens.weight"] # [vocab_size, hidden_size] + vocab_size, hidden_size = embed_weight.shape + logger.info(f"embed_tokens weight shape: {embed_weight.shape}") + + # Save as raw binary (float32, little-endian) + bin_path = onnx_dir / "embed_tokens.bin" + embed_weight.astype(np.float32).tofile(bin_path) + logger.info(f"embed_tokens.bin saved ({bin_path.stat().st_size / 1e6:.1f} MB)") + + # Save metadata + meta_path = onnx_dir / "embed_tokens.json" + with open(meta_path, "w") as f: + json.dump( + { + "vocab_size": int(vocab_size), + "hidden_size": int(hidden_size), + "dtype": "float32", + "byte_order": "little", + }, + f, + indent=2, + ) + logger.info("embed_tokens.json saved") + + return bin_path + + +# === 4. Decoder Export (builder) === + + +def export_decoder( + weights: dict[str, np.ndarray], config: dict, onnx_dir: pathlib.Path +) -> pathlib.Path: + """Export decoder.onnx (LFM2 backbone with inputs_embeds). + + Outputs both logits and hidden_states for audio generation. + """ + logger.info("Exporting decoder.onnx...") + + lfm_config = config.get("lfm", {}) + lfm2_config = LFM2Config.from_hf_config(type("Config", (), lfm_config)()) + + builder = LFM2Builder(lfm2_config, use_integrated_rope=True, vl_naming=True) + + # Load LFM weights (prefixed with "lfm.") + for name, weight in weights.items(): + if name.startswith("lfm."): + new_name = "model." + name[4:] + builder.weights[new_name] = weight + + H = lfm2_config.hidden_size + + # Build inputs + builder.inputs.append( + helper.make_tensor_value_info( + "inputs_embeds", TensorProto.FLOAT, ["batch_size", "sequence_length", H] + ) + ) + builder.inputs.append( + helper.make_tensor_value_info( + "attention_mask", TensorProto.INT64, ["batch_size", "total_sequence_length"] + ) + ) + + # Cache inputs + conv_set = set(builder.conv_indices) + attn_set = set(builder.attn_indices) + for idx in range(lfm2_config.num_hidden_layers): + if idx in conv_set: + builder.inputs.append( + helper.make_tensor_value_info( + f"past_conv.{idx}", + TensorProto.FLOAT, + ["batch_size", H, lfm2_config.conv_L_cache], + ) + ) + elif idx in attn_set: + builder.inputs.append( + helper.make_tensor_value_info( + f"past_key_values.{idx}.key", + TensorProto.FLOAT, + [ + "batch_size", + lfm2_config.num_key_value_heads, + "past_sequence_length", + builder.head_dim, + ], + ) + ) + builder.inputs.append( + helper.make_tensor_value_info( + f"past_key_values.{idx}.value", + TensorProto.FLOAT, + [ + "batch_size", + lfm2_config.num_key_value_heads, + "past_sequence_length", + builder.head_dim, + ], + ) + ) + + builder.build_outputs() + + # Add hidden_states output for audio generation + builder.outputs.append( + helper.make_tensor_value_info( + "hidden_states", TensorProto.FLOAT, ["batch_size", "sequence_length", H] + ) + ) + + builder.build_rope_cache() + builder.build_attention_mask_subgraph() + + builder.add_initializer( + "model.embed_tokens.weight", builder.weights["model.embed_tokens.weight"] + ) + hidden_state = "inputs_embeds" + + for layer_idx in range(lfm2_config.num_hidden_layers): + layer_type = lfm2_config.layer_types[layer_idx] + logger.info(f"Building decoder layer {layer_idx} ({layer_type})...") + builder.prepare_layer_weights(layer_idx, layer_type) + + if layer_type == "conv": + hidden_state = builder.build_conv_layer(layer_idx, hidden_state) + else: + hidden_state = builder.build_attention_layer(layer_idx, hidden_state) + + # Build lm_head and capture hidden states + # The build_lm_head applies final norm then lm_head projection + # We need the normed hidden states before projection + builder.build_lm_head(hidden_state) + + # Add Identity node to output hidden states (final norm output) + # The final norm output is at /model/layers.{num_layers}/final_norm_layernorm/output_0 + num_layers = lfm2_config.num_hidden_layers + final_norm_output = f"/model/layers.{num_layers}/final_norm_layernorm/output_0" + builder.nodes.append( + helper.make_node( + "Identity", + [final_norm_output], + ["hidden_states"], + name="/hidden_states/Identity", + ) + ) + + builder.build_value_info() + + graph = helper.make_graph( + builder.nodes, + "decoder", + builder.inputs, + builder.outputs, + builder.initializers, + value_info=builder.value_info, + ) + + model = helper.make_model( + graph, + opset_imports=[ + helper.make_opsetid("", 21), + helper.make_opsetid("com.microsoft", 1), + ], + ir_version=10, + ) + model.producer_name = "liquidonnx" + + output_path = onnx_dir / "decoder.onnx" + output_data = onnx_dir / "decoder.onnx_data" + if output_data.exists(): + output_data.unlink() + + onnx.save_model( + model, + str(output_path), + save_as_external_data=True, + all_tensors_to_one_file=True, + location="decoder.onnx_data", + size_threshold=1024, + ) + logger.info(f"decoder saved to {output_path}") + return output_path + + +# === Quantization === + + +def do_quantize(onnx_dir: pathlib.Path, bits: int, block_size: int, symmetric: bool): + """Quantize all exportable models to specified precision.""" + # Models to quantize: (relative_path, exclude_lm_head) + models_to_quantize = [ + ("decoder", True), + ("audio_encoder", False), + ("audio_embedding", False), + ("audio_detokenizer", False), + ("vocoder_depthformer", False), + ] + + for model_path, exclude_lm_head in models_to_quantize: + fp32_path = onnx_dir / f"{model_path}.onnx" + quant_path = onnx_dir / f"{model_path}_q{bits}.onnx" + + if not fp32_path.exists(): + continue + if quant_path.exists(): + logger.info(f" {model_path}_q{bits}.onnx already exists, skipping") + continue + + _, orig_mb = get_model_size(fp32_path) + quantize_model( + fp32_path, + quant_path, + bits=bits, + block_size=block_size, + exclude_lm_head=exclude_lm_head, + symmetric=symmetric, + ) + _, quant_mb = get_model_size(quant_path) + logger.info(f" {model_path}: {orig_mb:.1f} -> {quant_mb:.1f} MB") + + +# === FP16 Conversion === + + +def convert_to_fp16( + input_path: pathlib.Path, + output_path: pathlib.Path, + keep_io_types: bool = True, +): + """Convert ONNX model from FP32 to FP16. + + Args: + input_path: Path to FP32 ONNX model + output_path: Path for FP16 output model + keep_io_types: Keep inputs/outputs as FP32 for compatibility + """ + from onnx.external_data_helper import load_external_data_for_model + from onnxruntime.transformers.float16 import convert_float_to_float16 + + logger.info(f"Converting {input_path.name} to FP16...") + + model = onnx.load(str(input_path), load_external_data=False) + load_external_data_for_model(model, str(input_path.parent)) + + model_fp16 = convert_float_to_float16( + model, + keep_io_types=keep_io_types, + force_fp16_initializers=True, + disable_shape_infer=True, + ) + + output_data_path = output_path.parent / f"{output_path.stem}.onnx_data" + if output_data_path.exists(): + output_data_path.unlink() + + onnx.save_model( + model_fp16, + str(output_path), + save_as_external_data=True, + all_tensors_to_one_file=True, + location=f"{output_path.stem}.onnx_data", + ) + + orig_mb = get_total_model_size_mb(input_path) + fp16_mb = get_total_model_size_mb(output_path) + ratio = orig_mb / fp16_mb if fp16_mb > 0 else 0 + logger.info(f" {input_path.name}: {orig_mb:.1f} -> {fp16_mb:.1f} MB ({ratio:.1f}x)") + + +def do_fp16(onnx_dir: pathlib.Path): + """Convert all models to FP16.""" + models = [ + "decoder", + "audio_encoder", + "audio_embedding", + "audio_detokenizer", + "vocoder_depthformer", + ] + + for model_name in models: + fp32_path = onnx_dir / f"{model_name}.onnx" + fp16_path = onnx_dir / f"{model_name}_fp16.onnx" + + if not fp32_path.exists(): + continue + if fp16_path.exists(): + logger.info(f" {model_name}_fp16.onnx already exists, skipping") + continue + + convert_to_fp16(fp32_path, fp16_path) + + +# === 7. Audio Detokenizer Export === + + +def save_mel_config(onnx_dir: pathlib.Path): + """Save mel spectrogram configuration for ASR preprocessing. + + The mel filterbank and window are generated at runtime using librosa, + making inference compatible with transformers.js which cannot load numpy files. + + Parameters match liquid_audio's AudioToMelSpectrogramPreprocessor config. + """ + # Mel spectrogram parameters from LFM2.5-Audio config + mel_config = { + "sample_rate": 16000, + "n_fft": 512, + "win_length": 400, # window_size (0.025) * sample_rate + "hop_length": 160, # window_stride (0.01) * sample_rate + "n_mels": 128, + "fmin": 0, + "fmax": 8000, # sample_rate / 2 + "preemph": 0.97, + "log_zero_guard": 5.960464477539063e-08, # 2^-24 + "normalize": "per_feature", + "mel_norm": "slaney", + } + + # Save config only - filterbank and window generated at runtime via librosa + config_path = onnx_dir / "mel_config.json" + with open(config_path, "w") as f: + json.dump(mel_config, f, indent=2) + logger.info(f"Mel config saved to {config_path}") + + +# === Main Export === + + +def export_full_model(model_path: str, output_dir: pathlib.Path): + """Export all components of LFM2.5-Audio to ONNX. + + Exports: + - decoder.onnx: LFM2 backbone (includes embed_tokens.weight for text embedding) + - audio_encoder.onnx: Conformer encoder for ASR + - audio_embedding.onnx: Audio code embeddings for TTS + - audio_detokenizer.onnx: Neural vocoder for TTS + - vocoder_depthformer.onnx: Autoregressive audio codebook prediction + + Note: embed_tokens.weight is stored in decoder.onnx (used for tied LM head). + Inference extracts this weight for text embedding lookup. + """ + output_dir.mkdir(parents=True, exist_ok=True) + onnx_dir = output_dir / "onnx" + onnx_dir.mkdir(exist_ok=True) + + # Load config and weights + config = load_audio_config(model_path) + weights = load_audio_model_weights(model_path) + + # === Builder-based exports (no PyTorch model needed) === + export_audio_embedding(weights, config, onnx_dir) + export_audio_embedding_binary(weights, config, onnx_dir) # For direct lookup + export_embed_tokens(weights, config, onnx_dir) # For web inference + export_decoder(weights, config, onnx_dir) + export_audio_encoder_builder(model_path, config, onnx_dir) + export_vocoder_depthformer(model_path, onnx_dir) + export_audio_detokenizer_builder(model_path, onnx_dir) + save_mel_config(onnx_dir) + + # Clean up weights after builder exports + weights.clear() + gc.collect() + + # Copy config and tokenizer + from huggingface_hub import hf_hub_download + + for filename in ["config.json", "tokenizer.json", "tokenizer_config.json"]: + try: + src = hf_hub_download(model_path, filename) + shutil.copy(src, output_dir / filename) + except Exception as e: + logger.warning(f"Could not copy {filename}: {e}") + + # Print summary + logger.info("\n" + "=" * 60) + logger.info("Export Summary") + logger.info("=" * 60) + total_size = 0 + for fpath in sorted(onnx_dir.iterdir()): + if fpath.is_file(): + size = fpath.stat().st_size + total_size += size + logger.info(f" {fpath.name}: {size / 1e6:.1f} MB") + logger.info(f"Total: {total_size / 1e9:.2f} GB") + + return output_dir + + +def main(): + parser = argparse.ArgumentParser( + description="ONNX export for LFM2.5-Audio (ASR, TTS, Interleaved)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + parser.add_argument( + "model", + help="HuggingFace model ID (e.g., LiquidAI/LFM2.5-Audio-1.5B)", + ) + parser.add_argument( + "--output-dir", + type=pathlib.Path, + default=pathlib.Path("."), + help="Output base directory", + ) + parser.add_argument( + "--output-name", + type=str, + help="Output folder name (default: {model-name}-ONNX)", + ) + parser.add_argument( + "--precision", + nargs="*", + metavar="PRECISION", + help="Output precisions: fp16, q4, q8 (default if no args: fp16, q4, q8)", + ) + parser.add_argument( + "--block-size", + type=int, + default=32, + help="Block size for quantization (default: 32)", + ) + parser.add_argument( + "--split-data", + type=float, + default=2.0, + metavar="GB", + help="Split external data into chunks (default: 2GB per chunk)", + ) + + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + model_name = get_model_name(args.model) + output_name = args.output_name or f"{model_name}-ONNX" + output_dir = args.output_dir / "exports" / output_name + onnx_dir = output_dir / "onnx" + + logger.info("=" * 60) + logger.info("ONNX Export for LFM2.5-Audio") + logger.info("=" * 60) + + export_full_model(args.model, output_dir) + + # Parse precision options + do_fp16_conversion = False + quant_bits = [] + if args.precision is not None: + if len(args.precision) == 0: + # Default: export all precisions + do_fp16_conversion = True + quant_bits = [4, 8] + else: + for p in args.precision: + p = p.lower() + if p == "fp16": + do_fp16_conversion = True + elif p in ("q4", "q8"): + quant_bits.append(int(p[1])) + + # FP16 conversion + if do_fp16_conversion: + logger.info("=" * 60) + logger.info("Converting to FP16") + logger.info("=" * 60) + do_fp16(onnx_dir) + + # Quantization + for bits in quant_bits: + logger.info("=" * 60) + logger.info(f"Quantizing to Q{bits}") + logger.info("=" * 60) + do_quantize(onnx_dir, bits, args.block_size, symmetric=(bits == 4)) + + # Split data + chunk_size_bytes = int(args.split_data * 1024 * 1024 * 1024) + for onnx_file in onnx_dir.glob("*.onnx"): + data_file = onnx_file.with_suffix(".onnx_data") + if data_file.exists() and data_file.stat().st_size > chunk_size_bytes: + logger.info(f"Splitting {onnx_file.name}...") + split_external_data(onnx_file, chunk_size=chunk_size_bytes) + + logger.info("=" * 60) + logger.info("Export complete!") + logger.info("=" * 60) + logger.info(f"Output: {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/src/liquidonnx/lfm2_audio/infer.py b/src/liquidonnx/lfm2_audio/infer.py new file mode 100644 index 0000000..384679d --- /dev/null +++ b/src/liquidonnx/lfm2_audio/infer.py @@ -0,0 +1,1874 @@ +#!/usr/bin/env python3 +""" +ONNX inference for LFM2.5-Audio supporting all 3 modes: +- ASR (Automatic Speech Recognition): Audio → Text +- TTS (Text-to-Speech): Text → Audio +- Interleaved: Mixed text and audio I/O + +Uses ONNX models: +- decoder.onnx: LFM2 backbone (embeddings → logits/hidden_states) +- audio_encoder.onnx: Conformer encoder for ASR +- audio_embedding.onnx: Audio code embeddings for TTS +- audio_detokenizer.onnx: Audio codes → STFT features for waveform synthesis +- vocoder_depthformer.onnx: Autoregressive audio codebook prediction (8× per frame) + +All components use ONNX-only inference. + +Usage: + # Text generation + uv run lfm2-audio-infer /path/to/model --prompt "Hello world" + + # ASR: Transcribe audio to text + uv run lfm2-audio-infer /path/to/model --mode asr --audio input.wav + + # TTS: Generate audio from text + uv run lfm2-audio-infer /path/to/model --mode tts --prompt "Hello world" --output output.wav + + # Interleaved with text prompt + uv run lfm2-audio-infer /path/to/model --mode interleaved --prompt "Respond with audio" + + # Interleaved with audio input (recommended) + uv run lfm2-audio-infer /path/to/model --mode interleaved --audio input.wav --output output.wav +""" + +import argparse +import logging +import pathlib +import time + +import numpy as np +import onnxruntime as ort + +logger = logging.getLogger(__name__) + +# === Default System Prompts === +DEFAULT_SYSTEM_PROMPT_ASR = "Perform ASR." +DEFAULT_SYSTEM_PROMPT_TTS = "Perform TTS. Use the UK female voice." +DEFAULT_SYSTEM_PROMPT_INTERLEAVED = "Respond with interleaved text and audio." + +# Max tokens defaults (matching liquid-audio) +# Each audio frame = 80ms (6x upsampling in detokenizer, 320 hop, 24kHz) +# 1024 frames ≈ 82 seconds of audio +DEFAULT_MAX_TOKENS_AUDIO = 1024 # TTS and interleaved modes +DEFAULT_MAX_TOKENS_TEXT = 100 # ASR and text modes + + +def resolve_precision_files(precision: str | None) -> dict[str, str | None]: + """Resolve file names from precision shorthand. + + Args: + precision: One of "fp16", "q4", "q8", or None for default (fp32) + + Returns: + Dict mapping component name to filename (or None for default) + """ + if precision is None: + return { + "decoder": None, + "audio_embedding": None, + "audio_encoder": None, + "audio_detokenizer": None, + "vocoder_depthformer": None, + } + + precision = precision.lower() + if precision not in ("fp16", "q4", "q8"): + raise ValueError(f"Invalid precision: {precision}. Use fp16, q4, or q8.") + + return { + "decoder": f"decoder_{precision}.onnx", + "audio_embedding": f"audio_embedding_{precision}.onnx", + "audio_encoder": f"audio_encoder_{precision}.onnx", + "audio_detokenizer": f"audio_detokenizer_{precision}.onnx", + "vocoder_depthformer": f"vocoder_depthformer_{precision}.onnx", + } + + +def load_session(model_path: pathlib.Path) -> ort.InferenceSession: + """Load ONNX model as inference session.""" + providers = ["CPUExecutionProvider"] + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + return ort.InferenceSession(str(model_path), sess_options, providers=providers) + + +def load_embed_tokens_weight(onnx_dir: pathlib.Path) -> np.ndarray: + """Load embed_tokens.weight from exported binary file. + + Why load from binary instead of extracting from decoder.onnx? + + While Python CAN extract weights via `onnx.load()` + graph.initializer, + JavaScript CANNOT - ONNX Runtime Web only exposes inference APIs. + + By loading from the same binary file, both Python and JS use identical + artifacts and code paths, ensuring consistent behavior across platforms. + + Files: + embed_tokens.bin - raw float32 binary [vocab_size * hidden_size] + embed_tokens.json - metadata {vocab_size, hidden_size, dtype} + + Falls back to extracting from decoder.onnx for backwards compatibility. + """ + import json + + bin_path = onnx_dir / "embed_tokens.bin" + meta_path = onnx_dir / "embed_tokens.json" + + if bin_path.exists() and meta_path.exists(): + # Load from binary (same as JavaScript) + with open(meta_path) as f: + meta = json.load(f) + + weight = np.fromfile(bin_path, dtype=np.float32) + weight = weight.reshape(meta["vocab_size"], meta["hidden_size"]) + logger.info(f"Loaded embed_tokens from {bin_path.name}: {weight.shape}") + return weight + + # Fallback: extract from decoder.onnx (for backwards compatibility) + logger.warning("embed_tokens.bin not found, extracting from decoder.onnx") + import onnx + + decoder_path = onnx_dir / "decoder.onnx" + model = onnx.load(str(decoder_path), load_external_data=True) + + for initializer in model.graph.initializer: + if initializer.name == "model.embed_tokens.weight": + return onnx.numpy_helper.to_array(initializer) + + raise ValueError("embed_tokens.weight not found") + + +def load_audio_embedding_weight(onnx_dir: pathlib.Path) -> np.ndarray | None: + """Load audio_embedding.weight from exported binary file. + + Returns None if binary file not found (falls back to ONNX model). + + Files: + audio_embedding.bin - raw float32 binary [vocab_size * hidden_size] + audio_embedding.json - metadata {vocab_size, hidden_size, num_codebooks, ...} + """ + import json + + bin_path = onnx_dir / "audio_embedding.bin" + meta_path = onnx_dir / "audio_embedding.json" + + if not (bin_path.exists() and meta_path.exists()): + return None + + with open(meta_path) as f: + meta = json.load(f) + + weight = np.fromfile(bin_path, dtype=np.float32) + weight = weight.reshape(meta["vocab_size"], meta["hidden_size"]) + logger.info(f"Loaded audio_embedding from {bin_path.name}: {weight.shape}") + return weight + + +class LFM2AudioInference: + """ONNX inference for LFM2.5-Audio supporting all modes.""" + + # Special tokens (from tokenizer) + IM_END_TOKEN = 7 # <|im_end|> + AUDIO_START_TOKEN = 128 # <|audio_start|> + TEXT_START_TOKEN = 129 # <|text_start|> + TEXT_END_TOKEN = 130 # <|text_end|> + MIXED_START_TOKEN = 131 # <|mixed_start|> + MIXED_END_TOKEN = 132 # <|mixed_end|> + + def __init__( + self, + model_dir: pathlib.Path, + decoder_file: str | None = None, + audio_embedding_file: str | None = None, + audio_encoder_file: str | None = None, + audio_detokenizer_file: str | None = None, + vocoder_depthformer_file: str | None = None, + ): + self.model_dir = model_dir + self.onnx_dir = model_dir / "onnx" + + # Store file name for vocoder loading + self._vocoder_depthformer_file = vocoder_depthformer_file + + # Load tokenizer + from transformers import AutoTokenizer + + self.tokenizer = AutoTokenizer.from_pretrained(str(model_dir), trust_remote_code=True) + + # Resolve file paths (use provided or default) + decoder_path = self.onnx_dir / (decoder_file or "decoder.onnx") + audio_embedding_path = self.onnx_dir / (audio_embedding_file or "audio_embedding.onnx") + audio_encoder_path = self.onnx_dir / (audio_encoder_file or "audio_encoder.onnx") + audio_detokenizer_path = self.onnx_dir / ( + audio_detokenizer_file or "audio_detokenizer.onnx" + ) + + logger.info(f"Loading decoder from {decoder_path.name}...") + self.decoder_session = load_session(decoder_path) + + # Load embed_tokens.weight for text embedding lookup + logger.info("Loading embed_tokens.weight...") + self.embed_tokens_weight = load_embed_tokens_weight(self.onnx_dir) + + # Try loading audio embedding from binary (faster), fallback to ONNX + self.audio_embedding_weight = load_audio_embedding_weight(self.onnx_dir) + if self.audio_embedding_weight is not None: + self.audio_embed_session = None # Not needed when using binary + else: + logger.info(f"Loading audio_embedding from {audio_embedding_path.name}...") + self.audio_embed_session = load_session(audio_embedding_path) + + if audio_encoder_path.exists(): + logger.info(f"Loading audio_encoder from {audio_encoder_path.name}...") + self.audio_encoder_session = load_session(audio_encoder_path) + else: + logger.warning(f"{audio_encoder_path.name} not found, ASR mode unavailable") + self.audio_encoder_session = None + + if audio_detokenizer_path.exists(): + logger.info(f"Loading audio_detokenizer from {audio_detokenizer_path.name}...") + self.audio_detokenizer_session = load_session(audio_detokenizer_path) + else: + logger.warning(f"{audio_detokenizer_path.name} not found, TTS output unavailable") + self.audio_detokenizer_session = None + + # Load ONNX depthformer for autoregressive inference + self.onnx_depthformer = None + self._load_onnx_depthformer() + + self._load_config() + + def _load_config(self): + """Load model config from config.json.""" + import json + + config_path = self.model_dir / "config.json" + with open(config_path) as f: + config = json.load(f) + + lfm_config = config.get("lfm", {}) + self.hidden_size = lfm_config.get("hidden_size", 2048) + self.num_layers = lfm_config.get("num_hidden_layers", 16) + self.num_kv_heads = lfm_config.get("num_key_value_heads", 8) + self.head_dim = self.hidden_size // lfm_config.get("num_attention_heads", 32) + self.conv_L = lfm_config.get("conv_L_cache", 3) + self.layer_types = lfm_config.get("layer_types", []) + self.vocab_size = lfm_config.get("vocab_size", 65536) + + # Audio config + self.audio_vocab_size = 16392 # 8 codebooks * 2049 + self.num_codebooks = 8 + self.codebook_vocab = 2049 + + def _load_onnx_depthformer(self): + """Load ONNX vocoder model for autoregressive audio codebook prediction. + + vocoder_depthformer.onnx takes hidden_states [B, 2048] and generates + audio codebook logits. Called 8× per audio frame (one per codebook). + """ + depthformer_path = self.onnx_dir / ( + self._vocoder_depthformer_file or "vocoder_depthformer.onnx" + ) + + if not depthformer_path.exists(): + raise FileNotFoundError(f"Vocoder depthformer not found: {depthformer_path}") + + logger.info(f"Loading vocoder_depthformer from {depthformer_path.name}...") + + self.onnx_depthformer = load_session(depthformer_path) + logger.info("ONNX vocoder ready for TTS") + + def _init_cache(self, batch_size: int = 1) -> dict[str, np.ndarray]: + """Initialize KV cache for generation.""" + cache = {} + + for idx, layer_type in enumerate(self.layer_types): + if layer_type == "conv": + cache[f"past_conv.{idx}"] = np.zeros( + (batch_size, self.hidden_size, self.conv_L), dtype=np.float32 + ) + else: + cache[f"past_key_values.{idx}.key"] = np.zeros( + (batch_size, self.num_kv_heads, 0, self.head_dim), dtype=np.float32 + ) + cache[f"past_key_values.{idx}.value"] = np.zeros( + (batch_size, self.num_kv_heads, 0, self.head_dim), dtype=np.float32 + ) + + return cache + + def _update_cache(self, cache: dict, outputs: dict) -> dict: + for key in cache: + if key.startswith("past_conv."): + idx = int(key.split(".")[1]) + cache[key] = outputs[f"present_conv.{idx}"] + elif key.startswith("past_key_values."): + parts = key.split(".") + idx = int(parts[1]) + kv_type = parts[2] + cache[key] = outputs[f"present.{idx}.{kv_type}"] + return cache + + def _sample( + self, + logits: np.ndarray, + temperature: float, + top_p: float | None = None, + top_k: int | None = None, + ) -> int: + """Sample next token using temperature and optional top-p/top-k sampling. + + Args: + logits: Raw logits from model + temperature: Sampling temperature (0 = greedy) + top_p: Optional nucleus sampling threshold + top_k: Optional top-k sampling (matches liquid-audio audio_top_k=4) + """ + if temperature == 0: + return int(np.argmax(logits)) + + logits = logits / temperature + + # Apply top-k filtering before softmax (matches liquid-audio) + if top_k is not None and top_k > 0: + top_k_indices = np.argpartition(logits, -top_k)[-top_k:] + mask = np.full(logits.shape, -np.inf) + mask[top_k_indices] = logits[top_k_indices] + logits = mask + + exp_logits = np.exp(logits - np.max(logits)) + probs = exp_logits / exp_logits.sum() + + if top_p is not None: + # Nucleus (top-p) sampling + sorted_indices = np.argsort(probs)[::-1] + sorted_probs = probs[sorted_indices] + cumsum = np.cumsum(sorted_probs) + + cutoff_idx = np.searchsorted(cumsum, top_p) + 1 + top_indices = sorted_indices[:cutoff_idx] + top_probs = probs[top_indices] + top_probs = top_probs / top_probs.sum() + + return int(np.random.choice(top_indices, p=top_probs)) + else: + # Pure temperature sampling (matches liquid-audio) + return int(np.random.choice(len(probs), p=probs)) + + def _get_text_embeds(self, input_ids: np.ndarray) -> np.ndarray: + # [batch, seq_len] → [batch, seq_len, hidden] + return self.embed_tokens_weight[input_ids].astype(np.float32) + + def _get_audio_embeds(self, audio_codes: np.ndarray) -> np.ndarray: + """Get audio code embeddings. + + Uses direct numpy indexing if audio_embedding.bin is available, + otherwise falls back to ONNX model call. + + Args: + audio_codes: [batch, num_codebooks] token indices + + Returns: + embeddings: [batch, num_codebooks, hidden_size] + """ + if self.audio_embedding_weight is not None: + return self.audio_embedding_weight[audio_codes].astype(np.float32) + else: + return self.audio_embed_session.run(["audio_embeds"], {"audio_codes": audio_codes})[0] + + def _run_decoder( + self, embeds: np.ndarray, attention_mask: np.ndarray, cache: dict + ) -> tuple[np.ndarray, np.ndarray, dict]: + inputs = { + "inputs_embeds": embeds.astype(np.float32), + "attention_mask": attention_mask, + **cache, + } + + outputs = self.decoder_session.run(None, inputs) + output_names = [o.name for o in self.decoder_session.get_outputs()] + output_dict = dict(zip(output_names, outputs, strict=True)) + + logits = output_dict["logits"] + hidden_states = output_dict.get("hidden_states") + cache = self._update_cache(cache, output_dict) + + return logits, hidden_states, cache + + # End-of-audio token (same across all codebooks) + END_OF_AUDIO_TOKEN = 2048 + + def _sample_audio_codes( + self, + hidden_states: np.ndarray, + temperature: float = 0.9, + top_k: int | None = None, + ) -> np.ndarray: + """Sample audio codes using ONNX depthformer. + + Runs 8 autoregressive steps (one per codebook) to generate a full + audio frame. Token 2048 is the end-of-audio token. + + Args: + hidden_states: [batch, hidden_size] or [batch, 1, hidden_size] + temperature: Sampling temperature + top_k: Optional top-k sampling (e.g., 4 for liquid-audio interleaved) + + Returns: + codes: [batch, 8] audio codes for each codebook + """ + if self.onnx_depthformer is None: + raise RuntimeError( + "ONNX depthformer not available for TTS.\n" + "Ensure vocoder_depthformer.onnx is exported." + ) + + num_codebooks = 8 + num_layers = 6 + num_kv_heads = 8 + head_dim = 32 + + # Squeeze to [batch, hidden_size] if needed + if hidden_states.ndim == 3: + hidden_states = hidden_states.squeeze(1) + batch_size = hidden_states.shape[0] + + codes_list = [] + for b in range(batch_size): + embedding = hidden_states[b : b + 1] # [1, hidden_size] + + # Initialize KV cache for depthformer (6 layers) + past_keys = np.zeros((num_layers, 1, 0, num_kv_heads, head_dim), dtype=np.float32) + past_values = np.zeros((num_layers, 1, 0, num_kv_heads, head_dim), dtype=np.float32) + + out_tokens = [] + prev_token = 0 + + for i in range(num_codebooks): + logits, _, new_keys, new_values = self.onnx_depthformer.run( + ["logits", "depth_slices", "new_keys", "new_values"], + { + "hidden_states": embedding.astype(np.float32), + "step_idx": np.array(i, dtype=np.int64), + "prev_token": np.array([prev_token], dtype=np.int64), + "past_keys": past_keys, + "past_values": past_values, + }, + ) + + past_keys = new_keys + past_values = new_values + + # Sample from logits including end-of-audio token (2048) + # Use temperature + optional top_k sampling to match liquid-audio + all_logits = logits[0] + if temperature is None or temperature <= 0: + token = int(np.argmax(all_logits)) + else: + token = self._sample(all_logits, temperature, top_p=None, top_k=top_k) + + out_tokens.append(token) + # Pass token directly to embedding lookup (table has 2049 entries: 0-2048) + # Don't clamp - if model predicts 2048, next codebook should see that embedding + prev_token = token + + codes_list.append(out_tokens) + + return np.array(codes_list, dtype=np.int64) # [batch, 8] + + def _is_end_of_audio(self, frame_codes: np.ndarray, first_codebook_only: bool = False) -> bool: + """Check if audio frame indicates end of audio. + + Args: + frame_codes: [8] array of codebook tokens + first_codebook_only: If True, only check first codebook (for interleaved mode, + matching liquid-audio behavior). If False, check any codebook. + + Returns: + True if end-of-audio detected + """ + if first_codebook_only: + return frame_codes[0] == self.END_OF_AUDIO_TOKEN + return np.any(frame_codes >= self.END_OF_AUDIO_TOKEN) + + def decode_audio(self, codes: np.ndarray) -> np.ndarray: + """Decode audio codes to waveform using ONNX detokenizer. + + Args: + codes: Audio codes with shape [T, 8] where T is number of frames + + Returns: + Waveform as float32 numpy array in range [-1, 1] + """ + if self.audio_detokenizer_session is None: + raise RuntimeError("Audio detokenizer not loaded") + + n_fft = 1280 + hop_length = 320 + win_length = 1280 + n_fft_bins = n_fft // 2 + 1 + window = np.hanning(n_fft).astype(np.float32) + + # Transpose: [T, 8] → [8, T] and add batch dimension → [1, 8, T] + codes_t = codes.T.astype(np.int64) + codes_batch = codes_t[np.newaxis, :, :] + + # Run detokenizer: [1, 8, T] → [1, T, 1282] + stft_features = self.audio_detokenizer_session.run( + ["stft_features"], {"audio_codes": codes_batch} + )[0] + stft_features = stft_features[0] # [T, 1282] + + # Convert to complex STFT: [log_magnitude | angle] → complex + log_magnitude = stft_features[:, :n_fft_bins] + angle = stft_features[:, n_fft_bins:] + magnitude = np.exp(log_magnitude) + complex_stft = magnitude * np.exp(1j * angle) + + # ISTFT with 'same' padding + waveform = _istft_same_padding(complex_stft.T, n_fft, hop_length, win_length, window) + + # Normalize to [-1, 1] + max_val = np.abs(waveform).max() + if max_val > 0: + waveform = waveform / max_val * 0.9 + + return waveform.astype(np.float32) + + # === Text Generation === + + def generate_text( + self, + prompt: str, + max_new_tokens: int = 100, + temperature: float = 0.7, + top_p: float = 0.9, + ) -> str: + """Generate text from prompt.""" + input_ids = self.tokenizer.encode(prompt, return_tensors="np") + batch_size, seq_len = input_ids.shape + + embeds = self._get_text_embeds(input_ids) + cache = self._init_cache(batch_size) + + attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + logits, _, cache = self._run_decoder(embeds, attention_mask, cache) + + next_logits = logits[0, -1, : self.vocab_size] + next_token = self._sample(next_logits, temperature, top_p) + + generated_tokens = [next_token] + total_len = seq_len + 1 + + start_time = time.time() + + for _ in range(max_new_tokens - 1): + if next_token == self.tokenizer.eos_token_id: + break + + next_ids = np.array([[next_token]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len), dtype=np.int64) + + logits, _, cache = self._run_decoder(next_embeds, attention_mask, cache) + + next_logits = logits[0, -1, : self.vocab_size] + next_token = self._sample(next_logits, temperature, top_p) + + generated_tokens.append(next_token) + total_len += 1 + + elapsed = time.time() - start_time + tokens_per_sec = len(generated_tokens) / elapsed if elapsed > 0 else 0 + logger.info( + f"Generated {len(generated_tokens)} tokens in {elapsed:.2f}s ({tokens_per_sec:.1f} tok/s)" + ) + + return self.tokenizer.decode(generated_tokens, skip_special_tokens=True) + + # === ASR (Audio → Text) === + + def _compute_mel_features(self, audio_path: str) -> tuple[np.ndarray, np.ndarray]: + """Compute mel spectrogram features from audio file. + + Uses pure numpy implementation for portability to NPU backends. + + Returns: + mel_features: [1, time, 128] mel spectrogram + mel_lengths: [1] length array + """ + return compute_mel_spectrogram_numpy(audio_path, self.onnx_dir) + + def _format_asr_prompt(self, system_prompt: str = DEFAULT_SYSTEM_PROMPT_ASR) -> str: + """Format ASR system instruction using ChatML format. + + The audio embeddings will be inserted at the user position. + """ + return f"<|startoftext|><|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n" + + def _format_asr_suffix(self) -> str: + """Format the suffix after audio embeddings.""" + return "<|im_end|>\n<|im_start|>assistant\n" + + def transcribe( + self, + audio_path: str, + max_new_tokens: int = 100, + temperature: float = 0.7, + system_prompt: str = DEFAULT_SYSTEM_PROMPT_ASR, + ) -> str: + """Transcribe audio to text using ChatML format. + + The prompt structure is: + <|startoftext|><|im_start|>system + {system_prompt}<|im_end|> + <|im_start|>user + [AUDIO EMBEDDINGS]<|im_end|> + <|im_start|>assistant + [TRANSCRIPTION OUTPUT] + """ + if self.audio_encoder_session is None: + raise RuntimeError("audio_encoder not loaded, ASR unavailable") + + # Compute mel spectrogram using proper preprocessing + mel_features, mel_lengths = self._compute_mel_features(audio_path) + + # Encode audio + audio_embeds, _ = self.audio_encoder_session.run( + ["audio_embeddings", "audio_lengths"], + {"mel_spectrogram": mel_features.astype(np.float32), "mel_lengths": mel_lengths}, + ) + + # Build the prompt: prefix + audio + suffix + # 1. Encode prefix text (system + user start) + # Note: add_special_tokens=False since we include <|startoftext|> in the prompt + prefix_text = self._format_asr_prompt(system_prompt) + prefix_ids = self.tokenizer.encode( + prefix_text, return_tensors="np", add_special_tokens=False + ) + prefix_embeds = self._get_text_embeds(prefix_ids) + + # 2. Encode suffix text (user end + assistant start) + suffix_text = self._format_asr_suffix() + suffix_ids = self.tokenizer.encode( + suffix_text, return_tensors="np", add_special_tokens=False + ) + suffix_embeds = self._get_text_embeds(suffix_ids) + + # 3. Concatenate: prefix + audio + suffix + all_embeds = np.concatenate( + [prefix_embeds, audio_embeds, suffix_embeds], + axis=1, + ) + + # Run decoder with full context + batch_size = 1 + seq_len = all_embeds.shape[1] + cache = self._init_cache(batch_size) + + attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + logits, _, cache = self._run_decoder(all_embeds, attention_mask, cache) + + # Generate text tokens + next_logits = logits[0, -1, : self.vocab_size] + next_token = self._sample(next_logits, temperature, top_p=None) + + generated_tokens = [next_token] + total_len = seq_len + 1 + + for _ in range(max_new_tokens - 1): + if next_token == self.tokenizer.eos_token_id: + break + if next_token == self.IM_END_TOKEN: + break + + next_ids = np.array([[next_token]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len), dtype=np.int64) + + logits, _, cache = self._run_decoder(next_embeds, attention_mask, cache) + + next_logits = logits[0, -1, : self.vocab_size] + next_token = self._sample(next_logits, temperature, top_p=None) + + generated_tokens.append(next_token) + total_len += 1 + + return self.tokenizer.decode(generated_tokens, skip_special_tokens=True) + + # === TTS (Text → Audio) === + + def _format_tts_prompt(self, text: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT_TTS) -> str: + """Format text with TTS system instruction using ChatML format.""" + return ( + "<|startoftext|><|im_start|>system\n" + f"{system_prompt}<|im_end|>\n" + f"<|im_start|>user\n{text}<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + def synthesize( + self, + text: str, + max_new_tokens: int = 100, + audio_temperature: float = 0.8, + audio_top_k: int = 64, + text_temperature: float = 0.7, + system_prompt: str = DEFAULT_SYSTEM_PROMPT_TTS, + ) -> list[np.ndarray]: + """Synthesize audio from text using depthformer. + + The model first generates text tokens until it produces <|audio|>, + then switches to depthformer-based audio code generation. + + Args: + text: Text to synthesize. + max_new_tokens: Maximum number of new tokens (text + audio frames combined), + matching PyTorch reference behavior. + audio_temperature: Temperature for audio sampling (0 = greedy). + Default 0.8 matches liquid-audio's fixed TTS settings. + audio_top_k: Top-k sampling for audio (64 matches liquid-audio). + text_temperature: Temperature for text sampling (0 = greedy). + system_prompt: System prompt for TTS. + + Returns list of audio code frames (8 codes each). + Each frame is [8] array of codebook indices. + """ + if self.onnx_depthformer is None: + raise RuntimeError("ONNX depthformer not loaded, TTS unavailable") + + # Format prompt with TTS system instruction + # Note: add_special_tokens=False since we include <|startoftext|> in the prompt + prompt = self._format_tts_prompt(text, system_prompt) + input_ids = self.tokenizer.encode(prompt, return_tensors="np", add_special_tokens=False) + batch_size, seq_len = input_ids.shape + + # Get text embeddings and run decoder + embeds = self._get_text_embeds(input_ids) + cache = self._init_cache(batch_size) + + attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(embeds, attention_mask, cache) + total_len = seq_len + + # Track tokens generated (text + audio) to match PyTorch behavior + tokens_generated = 0 + + # === Phase 1: Generate text until <|audio|> token === + in_audio_mode = False + while tokens_generated < max_new_tokens: + last_logits = logits[0, -1, : self.vocab_size] + next_token = self._sample(last_logits, text_temperature, top_p=None) + + if next_token == self.tokenizer.eos_token_id: + logger.warning("Model produced EOS before audio, TTS may not work") + break + + tokens_generated += 1 + + if next_token == self.AUDIO_START_TOKEN: + logger.info("Model entered audio mode") + in_audio_mode = True + # Feed audio_start token to get hidden states for first audio frame + next_ids = np.array([[self.AUDIO_START_TOKEN]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + break + + # Continue text generation + next_ids = np.array([[next_token]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + + if not in_audio_mode: + logger.warning("Model did not enter audio mode, forcing audio generation") + # Force audio start token and feed it to decoder + next_ids = np.array([[self.AUDIO_START_TOKEN]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + tokens_generated += 1 + + # === Phase 2: Generate audio frames using depthformer === + audio_codes = [] + start_time = time.time() + + while tokens_generated < max_new_tokens: + # Get hidden states for the last position: [1, hidden_size] + last_hidden = hidden_states[0, -1:, :] # [1, hidden_size] + + # Sample audio codes (autoregressive sampling, matches liquid-audio) + frame_codes = self._sample_audio_codes( + last_hidden, audio_temperature, top_k=audio_top_k + ) # [1, 8] + + # Check for end-of-audio (first codebook only, matching liquid-audio) + if self._is_end_of_audio(frame_codes[0], first_codebook_only=True): + logger.info(f"End of audio detected at frame {len(audio_codes)}") + break + + audio_codes.append(frame_codes[0]) # [8] + tokens_generated += 1 + + # Feed back audio codes to continue generation + # Preserve 2048 (end-of-audio) for embedding lookup, clamp others to 0-2047 + clamped_codes = np.where( + frame_codes[0] == self.END_OF_AUDIO_TOKEN, + self.END_OF_AUDIO_TOKEN, + np.minimum(frame_codes[0], 2047), + ) + audio_tokens = np.array( + [ + [ + cb * self.codebook_vocab + int(clamped_codes[cb]) + for cb in range(self.num_codebooks) + ] + ], + dtype=np.int64, + ) + next_embeds = self._get_audio_embeds(audio_tokens).sum(axis=1, keepdims=True) + + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + + elapsed = time.time() - start_time + frames_per_sec = len(audio_codes) / elapsed if elapsed > 0 else 0 + logger.info( + f"Generated {len(audio_codes)} audio frames in {elapsed:.2f}s " + f"({frames_per_sec:.1f} frames/s)" + ) + + # Debug: analyze code distribution + if audio_codes: + codes_array = np.array(audio_codes) # [T, 8] + logger.info( + f"Audio codes stats: min={codes_array.min()}, max={codes_array.max()}, " + f"mean={codes_array.mean():.1f}, std={codes_array.std():.1f}" + ) + + return audio_codes + + # === Interleaved Mode === + + def _format_interleaved_prompt( + self, text: str, system_prompt: str = DEFAULT_SYSTEM_PROMPT_INTERLEAVED + ) -> str: + """Format text with interleaved system instruction using ChatML format.""" + return ( + "<|startoftext|><|im_start|>system\n" + f"{system_prompt}<|im_end|>\n" + f"<|im_start|>user\n{text}<|im_end|>\n" + "<|im_start|>assistant\n" + ) + + def generate_interleaved( + self, + prompt: str, + max_new_tokens: int = 20, + audio_temperature: float = 0, + text_temperature: float = 0, + system_prompt: str = DEFAULT_SYSTEM_PROMPT_INTERLEAVED, + ) -> tuple[str, list[np.ndarray]]: + """Generate interleaved text and audio from text prompt. + + Defaults match liquid-audio library defaults (greedy decoding). + """ + # Note: add_special_tokens=False since we include <|startoftext|> in the prompt + formatted_prompt = self._format_interleaved_prompt(prompt, system_prompt) + input_ids = self.tokenizer.encode( + formatted_prompt, return_tensors="np", add_special_tokens=False + ) + batch_size, seq_len = input_ids.shape + + embeds = self._get_text_embeds(input_ids) + cache = self._init_cache(batch_size) + + attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(embeds, attention_mask, cache) + + text_tokens = [] + audio_codes = [] + total_len = seq_len + in_audio_mode = False + + for _ in range(max_new_tokens): + last_logits = logits[0, -1, :] + + if in_audio_mode: + # Use ONNX depthformer to generate audio frame + if self.onnx_depthformer is None or hidden_states is None: + logger.warning("ONNX depthformer unavailable, exiting audio mode") + in_audio_mode = False + continue + + last_hidden = hidden_states[0, -1:, :] + + # Autoregressive sampling (matches reference) + frame_codes = self._sample_audio_codes(last_hidden, audio_temperature) + + # Check for end of audio (only first codebook, matching liquid-audio) + if self._is_end_of_audio(frame_codes[0], first_codebook_only=True): + logger.info(f"End of audio detected at frame {len(audio_codes)}") + # Set all codes to 2048 and feed back (matching liquid-audio) + frame_codes[0][:] = self.END_OF_AUDIO_TOKEN + in_audio_mode = False + + # Still need to feed back the end-of-audio embedding + audio_tokens = np.array( + [ + [ + cb * self.codebook_vocab + self.END_OF_AUDIO_TOKEN + for cb in range(self.num_codebooks) + ] + ], + dtype=np.int64, + ) + next_embeds = self._get_audio_embeds(audio_tokens).sum(axis=1, keepdims=True) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder( + next_embeds, attention_mask, cache + ) + total_len += 1 + continue + + audio_codes.append(frame_codes[0]) + + # Feed all 8 codebook tokens as a summed embedding + audio_tokens = np.array( + [ + [ + cb * self.codebook_vocab + int(frame_codes[0][cb]) + for cb in range(self.num_codebooks) + ] + ], + dtype=np.int64, + ) + next_embeds = self._get_audio_embeds(audio_tokens).sum(axis=1, keepdims=True) + + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + continue # Skip the decoder update at the end of the loop + else: + # Sample from text vocabulary + text_logits = last_logits[: self.vocab_size] + token = self._sample(text_logits, text_temperature, top_p=None) + + if token == self.tokenizer.eos_token_id: + break + + if token == self.AUDIO_START_TOKEN: + logger.info("Model entered audio mode") + in_audio_mode = True + # Feed audio_start token to get hidden states for first audio frame + next_ids = np.array([[self.AUDIO_START_TOKEN]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder( + next_embeds, attention_mask, cache + ) + total_len += 1 + text_tokens.append(token) + continue + + text_tokens.append(token) + next_embeds = self._get_text_embeds(np.array([[token]], dtype=np.int64)) + + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + + text_output = self.tokenizer.decode(text_tokens, skip_special_tokens=True) + logger.info(f"Generated {len(text_tokens)} text tokens, {len(audio_codes)} audio frames") + + return text_output, audio_codes + + def generate_interleaved_from_audio( + self, + audio_path: str, + max_new_tokens: int = 300, + text_temperature: float = 1.0, + audio_temperature: float = 1.0, + audio_top_k: int = 4, + system_prompt: str = DEFAULT_SYSTEM_PROMPT_INTERLEAVED, + ) -> tuple[str, list[np.ndarray]]: + """Generate interleaved text+audio response from audio input. + + Defaults match official liquid-audio demo (not library defaults). + Uses counter-based mode switching: + - interleaved_n_text = 6 (text tokens before switching to audio) + - interleaved_n_audio = 12 (audio frames before switching to text) + + Args: + audio_path: Path to input audio file + max_new_tokens: Maximum tokens to generate + text_temperature: Sampling temperature for text (1.0 matches liquid-audio) + audio_temperature: Sampling temperature for audio (1.0 matches liquid-audio) + audio_top_k: Top-k sampling for audio (4 matches liquid-audio) + system_prompt: System prompt for interleaved mode. + + Returns: + Tuple of (text_response, audio_codes) + """ + # Encode audio + mel_features, mel_lengths = self._compute_mel_features(audio_path) + audio_embeds, _ = self.audio_encoder_session.run( + ["audio_embeddings", "audio_lengths"], + {"mel_spectrogram": mel_features.astype(np.float32), "mel_lengths": mel_lengths}, + ) + + # Build prompt: system + user audio + assistant + prefix_text = ( + f"<|startoftext|><|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n" + ) + suffix_text = "<|im_end|>\n<|im_start|>assistant\n" + + prefix_ids = self.tokenizer.encode( + prefix_text, return_tensors="np", add_special_tokens=False + ) + suffix_ids = self.tokenizer.encode( + suffix_text, return_tensors="np", add_special_tokens=False + ) + + prefix_embeds = self._get_text_embeds(prefix_ids) + suffix_embeds = self._get_text_embeds(suffix_ids) + + # Concatenate: prefix + audio + suffix + all_embeds = np.concatenate([prefix_embeds, audio_embeds, suffix_embeds], axis=1) + + batch_size = 1 + seq_len = all_embeds.shape[1] + cache = self._init_cache(batch_size) + + attention_mask = np.ones((batch_size, seq_len), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(all_embeds, attention_mask, cache) + + # Generate with counter-based mode switching (matching liquid-audio) + INTERLEAVED_N_TEXT = 6 + INTERLEAVED_N_AUDIO = 12 + + text_tokens = [] + audio_codes = [] + total_len = seq_len + in_audio_mode = False + modality_left = INTERLEAVED_N_TEXT + text_done = False + + for step in range(max_new_tokens): + modality_left -= 1 + + if in_audio_mode: + if self.onnx_depthformer is None or hidden_states is None: + logger.warning("Depthformer unavailable, exiting audio mode") + in_audio_mode = False + modality_left = INTERLEAVED_N_TEXT + continue + + last_hidden = hidden_states[0, -1:, :] + frame_codes = self._sample_audio_codes( + last_hidden, temperature=audio_temperature, top_k=audio_top_k + ) + frame = frame_codes[0] + + # Switch back to text after N audio frames (if text not done) + if modality_left <= 0 and not text_done: + in_audio_mode = False + modality_left = INTERLEAVED_N_TEXT + + # Check for end of audio - first codebook == 2048 (matching liquid-audio) + # liquid-audio: if next_token[0] == 2048: next_token[:] = 2048; current_modality = TEXT + # NOTE: liquid-audio does NOT reset modality_left, just switches mode + if frame[0] == 2048: + logger.info(f"End of audio token at step {step}") + frame[:] = 2048 # Set all codes to 2048 (matching liquid-audio) + in_audio_mode = False + # NOTE: Don't reset modality_left - it continues from where it was + # Don't save this frame, but still feed it back (matching liquid-audio) + else: + # Save valid frame + clamped_frame = np.minimum(frame, 2047) + audio_codes.append(clamped_frame.copy()) + + if len(audio_codes) % 20 == 0: + logger.info(f"Generated {len(audio_codes)} audio frames...") + + # Get embeddings for next step (always feed back, even for 2048 frames) + # Use 2048 directly for end-of-audio, clamp others + feed_codes = np.where(frame == 2048, 2048, np.minimum(frame, 2047)) + audio_tokens = np.array( + [ + [ + cb * self.codebook_vocab + int(feed_codes[cb]) + for cb in range(self.num_codebooks) + ] + ], + dtype=np.int64, + ) + next_embeds = self._get_audio_embeds(audio_tokens).sum(axis=1, keepdims=True) + else: + # Generate text token + last_logits = logits[0, -1, :] + text_logits = last_logits[: self.vocab_size] + token = self._sample(text_logits, text_temperature, top_p=None) + + if token == self.tokenizer.eos_token_id or token == self.IM_END_TOKEN: + logger.info(f"End of turn at step {step}") + break + + if token == self.TEXT_END_TOKEN: + logger.info(f"Text end at step {step}") + text_done = True + + # Switch to audio after N text tokens OR text_end + if modality_left <= 0 or text_done: + in_audio_mode = True + modality_left = INTERLEAVED_N_AUDIO + + text_tokens.append(token) + next_ids = np.array([[token]], dtype=np.int64) + next_embeds = self._get_text_embeds(next_ids) + + # Update decoder + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = self._run_decoder(next_embeds, attention_mask, cache) + total_len += 1 + + text_output = self.tokenizer.decode(text_tokens, skip_special_tokens=True) + logger.info(f"Generated {len(text_tokens)} text tokens, {len(audio_codes)} audio frames") + + return text_output, audio_codes + + +# === Numpy Mel Spectrogram === + + +def _resample_audio(audio: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray: + """Resample audio to target sample rate. + + This is INPUT PREPROCESSING only - converts audio files to 16kHz before + mel spectrogram computation. Not part of the ONNX model. + + Uses torchaudio for best accuracy (matches PyTorch pipeline exactly). + Falls back to scipy if torchaudio is unavailable. + + Args: + audio: Audio waveform as numpy array + orig_sr: Original sample rate + target_sr: Target sample rate (16000 for this model) + + Returns: + Resampled audio as numpy array + """ + if orig_sr == target_sr: + return audio + + try: + import torch + import torchaudio + + audio_tensor = torch.from_numpy(audio).unsqueeze(0) # [1, samples] + resampled = torchaudio.functional.resample(audio_tensor, orig_sr, target_sr) + return resampled.squeeze(0).numpy() + except ImportError: + # Fallback to scipy (slightly different results due to FFT-based algorithm) + import scipy.signal + + num_samples = int(len(audio) * target_sr / orig_sr) + return scipy.signal.resample(audio, num_samples) + + +def compute_mel_spectrogram_numpy( + audio_path: str, + onnx_dir: pathlib.Path, +) -> tuple[np.ndarray, np.ndarray]: + """Compute mel spectrogram for ONNX audio encoder. + + This implementation matches liquid_audio's AudioToMelSpectrogramPreprocessor + for compatibility with the ONNX audio encoder. + + Args: + audio_path: Path to audio file (WAV) + onnx_dir: Path to ONNX directory containing mel_config.json + (mel filterbank and window are generated at runtime via librosa) + + Returns: + mel_features: [1, time, 128] mel spectrogram + mel_lengths: [1] length array + """ + import json + + import librosa + import scipy.io.wavfile + + # Mel spectrogram config (matching liquid_audio's AudioToMelSpectrogramPreprocessor) + # Load from config file if available, otherwise use defaults + config_path = onnx_dir / "mel_config.json" + if config_path.exists(): + with open(config_path) as f: + mel_config = json.load(f) + else: + mel_config = { + "sample_rate": 16000, + "n_fft": 512, + "win_length": 400, + "hop_length": 160, + "n_mels": 128, + "fmin": 0, + "fmax": 8000, + "preemph": 0.97, + "log_zero_guard": 5.960464477539063e-08, + "mel_norm": "slaney", + } + + # Extract config + target_sr = mel_config["sample_rate"] + n_fft = mel_config["n_fft"] + win_length = mel_config["win_length"] + hop_length = mel_config["hop_length"] + preemph = mel_config["preemph"] + log_zero_guard = mel_config["log_zero_guard"] + + # Generate mel filterbank at runtime using librosa (same as NeMo/liquid_audio) + mel_filterbank = librosa.filters.mel( + sr=target_sr, + n_fft=n_fft, + n_mels=mel_config.get("n_mels", 128), + fmin=mel_config.get("fmin", 0), + fmax=mel_config.get("fmax", target_sr // 2), + norm=mel_config.get("mel_norm", "slaney"), + ).astype(np.float32) + + # Generate Hann window at runtime + hann_window = np.hanning(win_length).astype(np.float32) + + # === 1. Load audio === + sample_rate, audio = scipy.io.wavfile.read(audio_path) + + # Convert to float32 in [-1, 1] + if audio.dtype == np.int16: + audio = audio.astype(np.float32) / 32768.0 + elif audio.dtype == np.int32: + audio = audio.astype(np.float32) / 2147483648.0 + elif audio.dtype == np.float64: + audio = audio.astype(np.float32) + + # Convert stereo to mono + if len(audio.shape) > 1: + audio = audio.mean(axis=1) + + # === 2. Resample to 16kHz (input preprocessing) === + audio = _resample_audio(audio, sample_rate, target_sr) + + # === 3. Pre-emphasis filter === + # y[t] = x[t] - preemph * x[t-1] + audio_preemph = np.concatenate([[audio[0]], audio[1:] - preemph * audio[:-1]]) + + # === 4. STFT (matching torch.stft with center=True) === + # Pad for center=True + pad_amount = n_fft // 2 + audio_padded = np.pad(audio_preemph, (pad_amount, pad_amount), mode="constant") + + # Frame the signal + num_frames = 1 + (len(audio_padded) - n_fft) // hop_length + frames = np.zeros((num_frames, n_fft), dtype=np.float32) + + # Center the window in the frame (matching torch.stft behavior) + pad_left = (n_fft - win_length) // 2 + padded_window = np.zeros(n_fft, dtype=np.float32) + padded_window[pad_left : pad_left + win_length] = hann_window + + for i in range(num_frames): + start = i * hop_length + frames[i] = audio_padded[start : start + n_fft] * padded_window + + # FFT + stft_complex = np.fft.rfft(frames, axis=1).T # [n_fft//2+1, time] + + # === 5. Magnitude and power spectrum === + magnitude = np.abs(stft_complex) # [freq, time] + power_spec = magnitude**2 + + # === 6. Apply mel filterbank === + # mel_filterbank: [n_mels, n_fft//2+1], power_spec: [n_fft//2+1, time] + mel_spec = np.dot(mel_filterbank, power_spec) # [n_mels, time] + + # === 7. Log with zero guard === + mel_spec = np.log(mel_spec + log_zero_guard) + + # === 8. Per-feature normalization === + # Compute valid length first (matches FilterbankFeatures.get_seq_len) + input_samples = len(audio) # after resampling, before padding + valid_len = input_samples // hop_length + + # Normalize using only valid frames (matching liquid-audio's normalize_batch) + # Uses Bessel's correction (N-1) for std + total_frames = mel_spec.shape[1] + if valid_len > 1: + valid_mel = mel_spec[:, :valid_len] + mel_mean = valid_mel.mean(axis=1, keepdims=True) + mel_std = ( + np.sqrt(np.sum((valid_mel - mel_mean) ** 2, axis=1, keepdims=True) / (valid_len - 1)) + + 1e-5 + ) + mel_spec = (mel_spec - mel_mean) / mel_std + # Zero out frames beyond valid length + if total_frames > valid_len: + mel_spec[:, valid_len:] = 0.0 + + # === 9. Format output === + # [n_mels, time] -> [1, time, n_mels] + mel_features = mel_spec.T[np.newaxis, :, :].astype(np.float32) + # Return actual number of frames (not valid_len which is used only for normalization) + # This matches liquid-audio's ChatState behavior which uses the actual tensor length + actual_frames = mel_features.shape[1] + mel_lengths = np.array([actual_frames], dtype=np.int64) + + logger.info(f"Computed mel spectrogram: {mel_features.shape}") + return mel_features, mel_lengths + + +def audio_codes_to_wav( + audio_codes: list[np.ndarray], + output_path: str, + model_dir: pathlib.Path | None = None, + sample_rate: int = 24000, + audio_detokenizer_file: str | None = None, +): + """Convert audio codes to WAV file using ONNX-only decoding. + + Uses ONNX audio_detokenizer + numpy ISTFT. No PyTorch required. + """ + if len(audio_codes) < 2: + logger.warning("Not enough audio codes to generate audio") + return False + + # Stack codes: [T, 8] → [8, T] + codes = np.stack(audio_codes, axis=0) # [T, 8] + codes = np.clip(codes, 0, 2047) + codes_transposed = codes.T # [8, T] + + if model_dir is None: + raise ValueError("model_dir required for ONNX decoding") + + onnx_dir = model_dir / "onnx" + detok_path = onnx_dir / (audio_detokenizer_file or "audio_detokenizer.onnx") + + if not detok_path.exists(): + raise FileNotFoundError(f"{detok_path.name} not found in {onnx_dir}") + + return _decode_audio_onnx_numpy( + codes_transposed, detok_path, onnx_dir, output_path, sample_rate + ) + + +class StreamingISTFT: + """Streaming ISTFT implementation matching llama.cpp mtmd_audio_streaming_istft.""" + + def __init__(self, n_fft: int, hop_length: int): + self.n_fft = n_fft + self.hop_length = hop_length + self.n_fft_bins = n_fft // 2 + 1 + + # Hann window (periodic) + self.hann_window = np.array( + [0.5 * (1.0 - np.cos(2.0 * np.pi * i / n_fft)) for i in range(n_fft)], + dtype=np.float32, + ) + + # Streaming state + self.overlap_buffer = np.zeros(n_fft, dtype=np.float32) + self.window_sum_buffer = np.zeros(n_fft, dtype=np.float32) + self.padding_to_remove = (n_fft - hop_length) // 2 + + def reset(self): + self.overlap_buffer.fill(0) + self.window_sum_buffer.fill(0) + self.padding_to_remove = (self.n_fft - self.hop_length) // 2 + + def process_frame(self, frame_spectrum: np.ndarray) -> np.ndarray: + """Process a single STFT frame. + + Args: + frame_spectrum: [n_fft_bins * 2] interleaved real/imag + + Returns: + output: up to hop_length samples + """ + # Build full complex spectrum for IFFT + ifft_in = np.zeros(self.n_fft, dtype=np.complex64) + + # Copy positive frequencies + for j in range(self.n_fft_bins): + ifft_in[j] = frame_spectrum[j * 2] + 1j * frame_spectrum[j * 2 + 1] + + # Mirror negative frequencies (conjugate) + for j in range(1, self.n_fft_bins - 1): + mirror_idx = self.n_fft - j + ifft_in[mirror_idx] = ifft_in[j].conjugate() + + # IFFT + ifft_out = np.fft.ifft(ifft_in).real.astype(np.float32) + + # Update window sum and overlap buffer + self.window_sum_buffer += self.hann_window * self.hann_window + self.overlap_buffer += ifft_out * self.hann_window + + # Extract hop_length samples with normalization + output = np.zeros(self.hop_length, dtype=np.float32) + for i in range(self.hop_length): + if self.window_sum_buffer[i] > 1e-8: + output[i] = self.overlap_buffer[i] / self.window_sum_buffer[i] + else: + output[i] = self.overlap_buffer[i] + + # Shift buffers left by hop_length + self.overlap_buffer = np.roll(self.overlap_buffer, -self.hop_length) + self.overlap_buffer[-self.hop_length :] = 0 + + self.window_sum_buffer = np.roll(self.window_sum_buffer, -self.hop_length) + self.window_sum_buffer[-self.hop_length :] = 0 + + # Remove padding if needed + if self.padding_to_remove > 0: + to_remove = min(self.padding_to_remove, len(output)) + output = output[to_remove:] + self.padding_to_remove -= to_remove + + return output + + def flush(self) -> np.ndarray: + """Flush remaining samples at end of stream.""" + output = [] + remaining = self.n_fft - self.hop_length + + while remaining > 0: + chunk_size = min(remaining, self.hop_length) + chunk = np.zeros(chunk_size, dtype=np.float32) + + for i in range(chunk_size): + if self.window_sum_buffer[i] > 1e-8: + chunk[i] = self.overlap_buffer[i] / self.window_sum_buffer[i] + else: + chunk[i] = self.overlap_buffer[i] + + output.append(chunk) + + # Shift buffers + self.overlap_buffer = np.roll(self.overlap_buffer, -chunk_size) + self.overlap_buffer[-chunk_size:] = 0 + self.window_sum_buffer = np.roll(self.window_sum_buffer, -chunk_size) + self.window_sum_buffer[-chunk_size:] = 0 + + remaining -= chunk_size + + return np.concatenate(output) if output else np.array([], dtype=np.float32) + + +def _istft_same_padding( + spec: np.ndarray, + n_fft: int, + hop_length: int, + win_length: int, + window: np.ndarray, +) -> np.ndarray: + """ISTFT with 'same' padding matching liquid_audio. + + This uses the same algorithm as liquid_audio/detokenizer.py ISTFT class + which pads to ensure output length matches input length * hop_length. + + Args: + spec: Complex STFT [freq, time] + n_fft: FFT size + hop_length: Hop length between frames + win_length: Window length + window: Window function array + + Returns: + Audio waveform as numpy array + """ + N, T = spec.shape + pad = (win_length - hop_length) // 2 + + # Inverse FFT + ifft = np.fft.irfft(spec, n_fft, axis=0, norm="backward") # [n_fft, T] + ifft = ifft * window[:, None] + + # Overlap and Add + output_size = (T - 1) * hop_length + win_length + audio = np.zeros(output_size) + for t in range(T): + start = t * hop_length + audio[start : start + win_length] += ifft[:, t] + + # Window envelope for normalization + window_sq = window**2 + window_envelope = np.zeros(output_size) + for t in range(T): + start = t * hop_length + window_envelope[start : start + win_length] += window_sq + + # Normalize and trim padding + audio_trimmed = audio[pad:-pad] / window_envelope[pad:-pad] + return audio_trimmed + + +def _decode_audio_onnx_numpy( + codes: np.ndarray, + detok_path: pathlib.Path, + onnx_dir: pathlib.Path, + output_path: str, + sample_rate: int, +) -> bool: + """Decode audio using ONNX detokenizer + numpy ISTFT. + + Pure numpy implementation - no PyTorch required. + Uses ISTFT with 'same' padding to match liquid_audio behavior. + """ + import scipy.io.wavfile + + # ISTFT parameters (fixed for this model) + n_fft = 1280 + hop_length = 320 + win_length = 1280 + n_fft_bins = n_fft // 2 + 1 + + # Generate Hann window at runtime (~18µs, faster than loading from disk) + window = np.hanning(n_fft).astype(np.float32) + + # Load ONNX detokenizer + detok_session = load_session(detok_path) + + # Run detokenizer: [1, 8, T] → [1, T*6, 1282] (6x upsampling) + # Input codes are already [8, T] from audio_codes_to_wav + codes_batch = codes[np.newaxis, :, :].astype(np.int64) # [1, 8, T] + stft_features = detok_session.run(["stft_features"], {"audio_codes": codes_batch})[0] + stft_features = stft_features[0] # [T*6, 1282] + + # Convert to complex STFT: [log_magnitude | angle] → complex + log_magnitude = stft_features[:, :n_fft_bins] + angle = stft_features[:, n_fft_bins:] + magnitude = np.exp(log_magnitude) + complex_stft = magnitude * np.exp(1j * angle) + + # ISTFT with 'same' padding + waveform = _istft_same_padding(complex_stft.T, n_fft, hop_length, win_length, window) + + # Normalize to prevent clipping + max_val = np.abs(waveform).max() + if max_val > 0: + waveform = waveform / max_val * 0.9 + + # Save as WAV + waveform_int16 = (waveform * 32767).astype(np.int16) + scipy.io.wavfile.write(output_path, sample_rate, waveform_int16) + + duration = len(waveform) / sample_rate + logger.info(f"Saved audio to {output_path} ({duration:.2f}s)") + return True + + +def main(): + parser = argparse.ArgumentParser( + description="LFM2.5-Audio ONNX inference (all modes)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + + parser.add_argument( + "model_dir", + type=pathlib.Path, + help="Path to exported ONNX model directory", + ) + parser.add_argument( + "--mode", + choices=["text", "asr", "tts", "interleaved"], + default="text", + help="Inference mode (default: text)", + ) + parser.add_argument( + "--prompt", + type=str, + default="The capital of France is", + help="Input prompt for text/tts/interleaved modes", + ) + parser.add_argument( + "--audio", + type=str, + help="Input audio file for ASR/interleaved modes", + ) + parser.add_argument( + "--output", + type=str, + help="Output audio file for TTS mode", + ) + parser.add_argument( + "--precision", + choices=["fp16", "q4", "q8"], + help="Model precision shorthand (default: fp32)", + ) + parser.add_argument( + "--decoder", + metavar="FILE", + help="Decoder ONNX file (relative to onnx/ dir)", + ) + parser.add_argument( + "--audio-embedding", + metavar="FILE", + help="Audio embedding ONNX file (relative to onnx/ dir)", + ) + parser.add_argument( + "--audio-encoder", + metavar="FILE", + help="Audio encoder ONNX file (relative to onnx/ dir)", + ) + parser.add_argument( + "--audio-detokenizer", + metavar="FILE", + help="Audio detokenizer ONNX file (relative to onnx/ dir)", + ) + parser.add_argument( + "--vocoder-depthformer", + metavar="FILE", + help="Vocoder depthformer ONNX file (relative to onnx/ dir)", + ) + parser.add_argument( + "--max-tokens", + type=int, + default=None, + help=f"Maximum tokens/frames to generate (default: {DEFAULT_MAX_TOKENS_AUDIO} for tts/interleaved, {DEFAULT_MAX_TOKENS_TEXT} for asr/text)", + ) + parser.add_argument( + "--temperature", + type=float, + default=None, + help="Text sampling temperature (default: 0 for ASR, 0.7 for TTS/text)", + ) + parser.add_argument( + "--audio-temperature", + type=float, + default=None, + help="Audio sampling temperature (default: 0.8 for TTS, 1.0 for interleaved)", + ) + parser.add_argument( + "--audio-top-k", + type=int, + default=None, + help="Top-k sampling for audio (default: 64 for TTS, 4 for interleaved)", + ) + parser.add_argument( + "--system", + type=str, + default=None, + help="System prompt (mode-specific defaults: ASR='Perform ASR.', " + "TTS='Perform TTS. Use the UK female voice.', " + "interleaved='Respond with interleaved text and audio.')", + ) + parser.add_argument( + "--save-codes", + type=str, + metavar="FILE", + help="Save audio codes to numpy file (.npy) for comparison with other decoders", + ) + parser.add_argument( + "--seed", + type=int, + default=42, + help="Random seed for reproducible generation (default: 42)", + ) + + args = parser.parse_args() + + # Apply mode-specific system prompt defaults + if args.system is None: + if args.mode == "asr": + args.system = DEFAULT_SYSTEM_PROMPT_ASR + elif args.mode == "tts": + args.system = DEFAULT_SYSTEM_PROMPT_TTS + elif args.mode == "interleaved": + args.system = DEFAULT_SYSTEM_PROMPT_INTERLEAVED + + # Apply mode-specific temperature defaults + if args.mode == "asr": + # ASR uses greedy decoding by default + if args.temperature is None: + args.temperature = 0 + elif args.mode == "interleaved": + # Interleaved uses 1.0 temperature (matching liquid-audio demo) + if args.temperature is None: + args.temperature = 1.0 + if args.audio_temperature is None: + args.audio_temperature = 1.0 + if args.audio_top_k is None: + args.audio_top_k = 4 # Matching liquid-audio interleaved + elif args.mode == "tts": + # TTS uses fixed sampling settings to match liquid-audio + if args.temperature is None: + args.temperature = 0.7 + if args.audio_temperature is None: + args.audio_temperature = 0.8 # Matching liquid-audio TTS + if args.audio_top_k is None: + args.audio_top_k = 64 # Matching liquid-audio TTS + else: + # Text mode + if args.temperature is None: + args.temperature = 0.7 + if args.audio_temperature is None: + args.audio_temperature = 0.8 + + # Apply mode-specific max_tokens defaults + if args.max_tokens is None: + if args.mode in ("interleaved", "tts"): + args.max_tokens = DEFAULT_MAX_TOKENS_AUDIO + else: + args.max_tokens = DEFAULT_MAX_TOKENS_TEXT + + logging.basicConfig(level=logging.INFO) + + # Set random seed for reproducibility + np.random.seed(args.seed) + logger.info(f"Random seed: {args.seed}") + + # Resolve component files from --precision + files = resolve_precision_files(args.precision) + + # Explicit file args override --precision + if args.decoder: + files["decoder"] = args.decoder + if args.audio_embedding: + files["audio_embedding"] = args.audio_embedding + if args.audio_encoder: + files["audio_encoder"] = args.audio_encoder + if args.audio_detokenizer: + files["audio_detokenizer"] = args.audio_detokenizer + if args.vocoder_depthformer: + files["vocoder_depthformer"] = args.vocoder_depthformer + + logger.info(f"Loading model from {args.model_dir}...") + model = LFM2AudioInference( + args.model_dir, + decoder_file=files["decoder"], + audio_embedding_file=files["audio_embedding"], + audio_encoder_file=files["audio_encoder"], + audio_detokenizer_file=files["audio_detokenizer"], + vocoder_depthformer_file=files["vocoder_depthformer"], + ) + + if args.mode == "text": + logger.info("Mode: Text Generation") + logger.info(f"Prompt: {args.prompt}") + output = model.generate_text( + args.prompt, + max_new_tokens=args.max_tokens, + temperature=args.temperature, + ) + print("\n" + "=" * 60) + print(f"Input: {args.prompt}") + print(f"Output: {output}") + print("=" * 60) + + elif args.mode == "asr": + if not args.audio: + parser.error("ASR mode requires --audio argument") + logger.info("Mode: ASR (Speech Recognition)") + logger.info(f"Audio: {args.audio}") + logger.info(f"System prompt: {args.system}") + transcription = model.transcribe( + args.audio, + max_new_tokens=args.max_tokens, + temperature=args.temperature, + system_prompt=args.system, + ) + print("\n" + "=" * 60) + print(f"Audio: {args.audio}") + print(f"Transcription: {transcription}") + print("=" * 60) + + elif args.mode == "tts": + logger.info("Mode: TTS (Text-to-Speech)") + logger.info(f"Text: {args.prompt}") + logger.info(f"System prompt: {args.system}") + logger.info( + f"Audio sampling: temperature={args.audio_temperature}, top_k={args.audio_top_k}" + ) + audio_codes = model.synthesize( + args.prompt, + max_new_tokens=args.max_tokens, + audio_temperature=args.audio_temperature, + audio_top_k=args.audio_top_k, + text_temperature=args.temperature, + system_prompt=args.system, + ) + print("\n" + "=" * 60) + print(f"Input: {args.prompt}") + print(f"Generated {len(audio_codes)} audio frames") + + if args.save_codes and audio_codes: + codes_array = np.stack(audio_codes, axis=0) # [T, 8] + np.save(args.save_codes, codes_array) + print(f"Codes: {args.save_codes} {codes_array.shape}") + + if args.output and audio_codes: + if audio_codes_to_wav( + audio_codes, + args.output, + model_dir=args.model_dir, + audio_detokenizer_file=files["audio_detokenizer"], + ): + print(f"Output: {args.output}") + print("=" * 60) + + elif args.mode == "interleaved": + logger.info("Mode: Interleaved") + logger.info(f"System prompt: {args.system}") + if args.audio: + # Audio input mode (matching liquid-audio demo) + logger.info(f"Audio: {args.audio}") + text_output, audio_codes = model.generate_interleaved_from_audio( + args.audio, + max_new_tokens=args.max_tokens, + text_temperature=args.temperature, + audio_temperature=args.audio_temperature, + system_prompt=args.system, + ) + print("\n" + "=" * 60) + print(f"Audio input: {args.audio}") + else: + # Text prompt mode + logger.info(f"Prompt: {args.prompt}") + text_output, audio_codes = model.generate_interleaved( + args.prompt, + max_new_tokens=args.max_tokens, + audio_temperature=args.audio_temperature, + text_temperature=args.temperature, + system_prompt=args.system, + ) + print("\n" + "=" * 60) + print(f"Input: {args.prompt}") + + print(f"Text: {text_output}") + print(f"Audio: {len(audio_codes)} frames") + + if args.save_codes and audio_codes: + codes_array = np.stack(audio_codes, axis=0) # [T, 8] + np.save(args.save_codes, codes_array) + print(f"Codes: {args.save_codes} {codes_array.shape}") + + if args.output and audio_codes: + if audio_codes_to_wav( + audio_codes, + args.output, + model_dir=args.model_dir, + audio_detokenizer_file=files["audio_detokenizer"], + ): + print(f"Output: {args.output}") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/src/liquidonnx/lfm2_moe/export.py b/src/liquidonnx/lfm2_moe/export.py index 330efed..b1cee0d 100644 --- a/src/liquidonnx/lfm2_moe/export.py +++ b/src/liquidonnx/lfm2_moe/export.py @@ -549,7 +549,7 @@ def main(): args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) # Derive output name from model path model_name = get_model_name(args.model) diff --git a/src/liquidonnx/lfm2_moe/infer.py b/src/liquidonnx/lfm2_moe/infer.py index 8a3b847..8fc1d15 100644 --- a/src/liquidonnx/lfm2_moe/infer.py +++ b/src/liquidonnx/lfm2_moe/infer.py @@ -23,7 +23,7 @@ def main(): parser.add_argument("--cpu", action="store_true", help="Force CPU execution (skip CUDA)") args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) model = ONNXTextModel(args.model, force_cpu=args.cpu) model.load() diff --git a/src/liquidonnx/lfm2_vl/benchmark.py b/src/liquidonnx/lfm2_vl/benchmark.py index 92d6119..fd9deb0 100644 --- a/src/liquidonnx/lfm2_vl/benchmark.py +++ b/src/liquidonnx/lfm2_vl/benchmark.py @@ -477,7 +477,7 @@ def main(): parser.add_argument("--compare", help="Path to community model for comparison") args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) image = None if args.image: diff --git a/src/liquidonnx/lfm2_vl/export.py b/src/liquidonnx/lfm2_vl/export.py index 8c99aeb..6153138 100644 --- a/src/liquidonnx/lfm2_vl/export.py +++ b/src/liquidonnx/lfm2_vl/export.py @@ -593,7 +593,7 @@ def main(): args = parser.parse_args() - logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s") + logging.basicConfig(level=logging.INFO) # Parse output precisions quant_bits = [] diff --git a/src/liquidonnx/quantize.py b/src/liquidonnx/quantize.py index 5faade5..af5294c 100644 --- a/src/liquidonnx/quantize.py +++ b/src/liquidonnx/quantize.py @@ -36,9 +36,27 @@ def get_model_size(path: pathlib.Path) -> tuple[float, float]: def get_total_model_size_mb(path: pathlib.Path) -> float: - """Return total model size in MB (model + external data).""" - model_mb, data_mb = get_model_size(path) - return model_mb + data_mb + """Return total model size in MB (model + all external data files). + + Handles split external data files (e.g., model.onnx_data, model.onnx_data_1). + """ + total = path.stat().st_size / 1e6 if path.exists() else 0 + + # Check for external data files (model.onnx_data, model.onnx_data_1, etc.) + base_data = path.with_suffix(".onnx_data") + if base_data.exists(): + total += base_data.stat().st_size / 1e6 + + # Check for split data files + i = 1 + while True: + split_data = path.parent / f"{path.stem}.onnx_data_{i}" + if not split_data.exists(): + break + total += split_data.stat().st_size / 1e6 + i += 1 + + return total def _rename_quantized_weights(model: onnx.ModelProto): diff --git a/tests/test_lfm2_audio/__init__.py b/tests/test_lfm2_audio/__init__.py new file mode 100644 index 0000000..bda2f27 --- /dev/null +++ b/tests/test_lfm2_audio/__init__.py @@ -0,0 +1 @@ +"""LFM2.5-Audio model tests.""" diff --git a/tests/test_lfm2_audio/conftest.py b/tests/test_lfm2_audio/conftest.py new file mode 100644 index 0000000..a3df650 --- /dev/null +++ b/tests/test_lfm2_audio/conftest.py @@ -0,0 +1,114 @@ +"""LFM2.5-Audio test fixtures.""" + +import gc +import logging +import pathlib + +import pytest +import torch + +logger = logging.getLogger(__name__) + +AUDIO_MODEL_ID = "LiquidAI/LFM2.5-Audio-1.5B" + + +@pytest.fixture(scope="module") +def reference_model(): + """Load reference PyTorch audio model. + + Returns (model, processor) tuple. + """ + from liquid_audio import LFM2AudioModel, LFM2AudioProcessor + + device = "cpu" + dtype = torch.float32 + + logger.info(f"Loading reference model from {AUDIO_MODEL_ID}...") + model = LFM2AudioModel.from_pretrained( + AUDIO_MODEL_ID, + dtype=dtype, + device=device, + ) + model.eval() + + processor = LFM2AudioProcessor.from_pretrained( + AUDIO_MODEL_ID, + device=device, + ) + # Fix device mismatch for audio_detokenizer + processor.audio_detokenizer.to(device) + + yield model, processor + + logger.info("Cleaning up reference model...") + del model + del processor + gc.collect() + + +@pytest.fixture(scope="module") +def onnx_model(exports_dir: pathlib.Path): + """Load ONNX audio model (fp32). + + Returns LFM2AudioInference instance. + """ + from liquidonnx.lfm2_audio.infer import LFM2AudioInference + + model_dir = exports_dir / "LFM2.5-Audio-1.5B-ONNX" + + if not model_dir.exists(): + pytest.skip( + f"ONNX model not found: {model_dir}\n" + f"Export with: uv run lfm2-audio-export {AUDIO_MODEL_ID}" + ) + + logger.info(f"Loading ONNX model from {model_dir}...") + model = LFM2AudioInference(model_dir) + + yield model + + logger.info("Cleaning up ONNX model...") + del model + gc.collect() + + +@pytest.fixture(scope="module") +def audio_processor(): + """Load audio processor for decoding. + + Returns LFM2AudioProcessor instance. + """ + from liquid_audio import LFM2AudioProcessor + + device = "cpu" + processor = LFM2AudioProcessor.from_pretrained( + AUDIO_MODEL_ID, + device=device, + ) + processor.audio_detokenizer.to(device) + + yield processor + + del processor + gc.collect() + + +@pytest.fixture(scope="session") +def sample_audio_short(exports_dir: pathlib.Path) -> pathlib.Path: + """Short sample audio file for testing.""" + # Navigate from exports/ to samples/ + base_dir = exports_dir.parent + audio_path = base_dir / "samples" / "audio" / "woodworks_question.wav" + if not audio_path.exists(): + pytest.skip(f"Sample audio not found: {audio_path}") + return audio_path + + +@pytest.fixture(scope="session") +def sample_audio_long(exports_dir: pathlib.Path) -> pathlib.Path: + """Longer sample audio file for testing.""" + base_dir = exports_dir.parent + audio_path = base_dir / "samples" / "audio" / "fool_me_once_mono.wav" + if not audio_path.exists(): + pytest.skip(f"Sample audio not found: {audio_path}") + return audio_path diff --git a/tests/test_lfm2_audio/test_asr.py b/tests/test_lfm2_audio/test_asr.py new file mode 100644 index 0000000..ef0fa6b --- /dev/null +++ b/tests/test_lfm2_audio/test_asr.py @@ -0,0 +1,164 @@ +""" +ASR (Automatic Speech Recognition) tests for LFM2.5-Audio ONNX exports. + +Tests transcription quality across all precisions (fp32, fp16, q4, q8). + +Run with: + uv run pytest tests/test_lfm2_audio/test_asr.py -v + uv run pytest tests/test_lfm2_audio/test_asr.py -v -k "fp16" + uv run pytest tests/test_lfm2_audio/test_asr.py -v -k "short" +""" + +import logging +import pathlib + +import pytest + +logger = logging.getLogger(__name__) + +# Precision configurations +PRECISION_CONFIGS = [ + pytest.param(None, id="fp32"), + pytest.param("fp16", id="fp16"), + pytest.param("q4", id="q4"), + pytest.param("q8", id="q8"), +] + +# Expected transcription keywords for validation +ASR_KEYWORDS = { + "fool_me_once_mono.wav": ["tennessee", "texas", "fool"], + "woodworks_question.wav": ["woodwork", "slogan", "tagline"], +} + + +def get_model_dir(exports_dir: pathlib.Path) -> pathlib.Path: + """Get the ONNX model directory.""" + return exports_dir / "LFM2.5-Audio-1.5B-ONNX" + + +def load_onnx_model(model_dir: pathlib.Path, precision: str | None): + """Load ONNX model with specified precision.""" + from liquidonnx.lfm2_audio.infer import LFM2AudioInference, resolve_precision_files + + files = resolve_precision_files(precision) + return LFM2AudioInference( + model_dir, + decoder_file=files["decoder"], + audio_embedding_file=files["audio_embedding"], + audio_encoder_file=files["audio_encoder"], + audio_detokenizer_file=files["audio_detokenizer"], + vocoder_depthformer_file=files["vocoder_depthformer"], + ) + + +def check_precision_available(model_dir: pathlib.Path, precision: str | None) -> bool: + """Check if the precision models are available.""" + onnx_dir = model_dir / "onnx" + if precision is None: + return (onnx_dir / "decoder.onnx").exists() + return (onnx_dir / f"decoder_{precision}.onnx").exists() + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_asr_short_audio( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, + precision: str | None, +): + """Test ASR transcription on short audio across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + logger.info(f"Testing ASR ({precision or 'fp32'}): {sample_audio_short.name}") + + transcription = model.transcribe(str(sample_audio_short)) + + logger.info(f" Transcription: {transcription}") + + # Validate output + assert transcription is not None + assert len(transcription) > 0 + assert isinstance(transcription, str) + + # Check for expected keywords + audio_name = sample_audio_short.name + if audio_name in ASR_KEYWORDS: + text_lower = transcription.lower() + found_keywords = [kw for kw in ASR_KEYWORDS[audio_name] if kw in text_lower] + assert len(found_keywords) > 0, ( + f"Expected keywords {ASR_KEYWORDS[audio_name]} not found in: {transcription}" + ) + logger.info(f" Found keywords: {found_keywords}") + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_asr_long_audio( + exports_dir: pathlib.Path, + sample_audio_long: pathlib.Path, + precision: str | None, +): + """Test ASR transcription on longer audio across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + logger.info(f"Testing ASR ({precision or 'fp32'}): {sample_audio_long.name}") + + transcription = model.transcribe(str(sample_audio_long)) + + logger.info(f" Transcription: {transcription[:100]}...") + + # Validate output + assert transcription is not None + assert len(transcription) > 20 # Long audio should produce substantial text + + # Check for expected keywords + audio_name = sample_audio_long.name + if audio_name in ASR_KEYWORDS: + text_lower = transcription.lower() + found_keywords = [kw for kw in ASR_KEYWORDS[audio_name] if kw in text_lower] + assert len(found_keywords) > 0, ( + f"Expected keywords {ASR_KEYWORDS[audio_name]} not found in: {transcription}" + ) + logger.info(f" Found keywords: {found_keywords}") + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_asr_produces_text( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, + precision: str | None, +): + """Test that ASR produces non-empty text output.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + transcription = model.transcribe(str(sample_audio_short)) + + # Basic sanity checks + assert transcription is not None + assert isinstance(transcription, str) + assert len(transcription.strip()) > 0, "ASR produced empty transcription" + # Should contain actual words (not just special tokens) + words = transcription.split() + assert len(words) >= 3, f"ASR produced too few words: {words}" + + del model diff --git a/tests/test_lfm2_audio/test_interleaved.py b/tests/test_lfm2_audio/test_interleaved.py new file mode 100644 index 0000000..a2a48bd --- /dev/null +++ b/tests/test_lfm2_audio/test_interleaved.py @@ -0,0 +1,421 @@ +""" +Interleaved mode tests for LFM2.5-Audio ONNX exports. + +Tests mixed text/audio generation across all precisions (fp32, fp16, q4, q8). + +Run with: + uv run pytest tests/test_lfm2_audio/test_interleaved.py -v + uv run pytest tests/test_lfm2_audio/test_interleaved.py -v -k "fp16" + uv run pytest tests/test_lfm2_audio/test_interleaved.py -v -k "audio_input" +""" + +import logging +import pathlib + +import numpy as np +import pytest +import torch + +logger = logging.getLogger(__name__) + +# Precision configurations +PRECISION_CONFIGS = [ + pytest.param(None, id="fp32"), + pytest.param("fp16", id="fp16"), + pytest.param("q4", id="q4"), + pytest.param("q8", id="q8"), +] + +# Text prompts for interleaved mode +TEXT_PROMPTS = [ + pytest.param("Say hello in a friendly way", id="hello"), + pytest.param("What is 2 plus 2?", id="math"), +] + + +def get_model_dir(exports_dir: pathlib.Path) -> pathlib.Path: + """Get the ONNX model directory.""" + return exports_dir / "LFM2.5-Audio-1.5B-ONNX" + + +def load_onnx_model(model_dir: pathlib.Path, precision: str | None): + """Load ONNX model with specified precision.""" + from liquidonnx.lfm2_audio.infer import LFM2AudioInference, resolve_precision_files + + files = resolve_precision_files(precision) + return LFM2AudioInference( + model_dir, + decoder_file=files["decoder"], + audio_embedding_file=files["audio_embedding"], + audio_encoder_file=files["audio_encoder"], + audio_detokenizer_file=files["audio_detokenizer"], + vocoder_depthformer_file=files["vocoder_depthformer"], + ) + + +def check_precision_available(model_dir: pathlib.Path, precision: str | None) -> bool: + """Check if the precision models are available.""" + onnx_dir = model_dir / "onnx" + if precision is None: + return (onnx_dir / "decoder.onnx").exists() + return (onnx_dir / f"decoder_{precision}.onnx").exists() + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_interleaved_audio_input( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, + precision: str | None, +): + """Test interleaved mode with audio input across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + logger.info(f"Testing interleaved ({precision or 'fp32'}): {sample_audio_short.name}") + + # Run interleaved with audio input + text_output, audio_codes = model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=200, + audio_temperature=0.8, + text_temperature=0.7, + ) + + if len(text_output) > 100: + logger.info(f" Text: {text_output[:100]}...") + else: + logger.info(f" Text: {text_output}") + logger.info(f" Audio frames: {len(audio_codes)}") + + # Validate outputs + assert text_output is not None + # Interleaved should produce either text or audio (or both) + assert len(text_output) > 0 or len(audio_codes) > 0, "No output generated" + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +@pytest.mark.parametrize("prompt", TEXT_PROMPTS) +def test_interleaved_text_input( + exports_dir: pathlib.Path, + precision: str | None, + prompt: str, +): + """Test interleaved mode with text-only input across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + logger.info(f"Testing interleaved text-only ({precision or 'fp32'}): '{prompt}'") + + # Run interleaved with text input only + text_output, audio_codes = model.generate_interleaved( + prompt=prompt, + max_new_tokens=200, + audio_temperature=0.8, + text_temperature=0.7, + ) + + if len(text_output) > 100: + logger.info(f" Text: {text_output[:100]}...") + else: + logger.info(f" Text: {text_output}") + logger.info(f" Audio frames: {len(audio_codes)}") + + # Validate outputs + assert text_output is not None + # Should produce either text or audio response + assert len(text_output) > 0 or len(audio_codes) > 0, "No output generated" + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_interleaved_produces_audio( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, + precision: str | None, +): + """Test that interleaved mode can produce audio output.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + # Use audio input which is more likely to produce audio response + text_output, audio_codes = model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=300, + audio_temperature=0.8, + text_temperature=0.7, + ) + + logger.info(f" Text length: {len(text_output)}, Audio frames: {len(audio_codes)}") + + # At minimum, should produce some output + total_output = len(text_output) + len(audio_codes) + assert total_output > 0, "Interleaved produced no output" + + # If audio was produced, validate format + if len(audio_codes) > 0: + for i, frame in enumerate(audio_codes): + assert frame.shape == (8,), f"Frame {i} has wrong shape: {frame.shape}" + assert np.all(frame >= 0), f"Frame {i} has negative values" + assert np.all(frame < 2048), f"Frame {i} has values >= 2048" + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_interleaved_audio_decoding( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, + precision: str | None, +): + """Test that interleaved audio output can be decoded to waveform.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + text_output, audio_codes = model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=300, + audio_temperature=0.8, + text_temperature=0.7, + ) + + if len(audio_codes) == 0: + pytest.skip("No audio generated in this run") + + # Decode to waveform + codes_array = np.stack(audio_codes, axis=0) # [T, 8] + waveform = model.decode_audio(codes_array) + + logger.info(f" Decoded {len(audio_codes)} frames -> {len(waveform)} samples") + + # Validate waveform + assert len(waveform) > 0, "Empty waveform" + assert waveform.dtype == np.float32, f"Wrong dtype: {waveform.dtype}" + assert np.abs(waveform).max() <= 1.5, "Waveform values out of range" + + del model + + +# === Reference Comparison Tests (fp32 only) === + + +def load_audio_tensor(audio_path: str, device: str = "cpu") -> tuple: + """Load audio file and return tensor + sample rate.""" + from scipy.io import wavfile + + sample_rate, audio_data = wavfile.read(audio_path) + + # Convert to float32 tensor normalized to [-1, 1] + if audio_data.dtype == np.int16: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 32768.0 + elif audio_data.dtype == np.int32: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) / 2147483648.0 + else: + audio_tensor = torch.tensor(audio_data, dtype=torch.float32) + + # Add batch dimension: [samples] → [1, samples] + audio_tensor = audio_tensor.unsqueeze(0).to(device) + return audio_tensor, sample_rate + + +def generate_reference_interleaved(model, processor, audio_path: str, max_new_tokens: int = 100): + """Generate interleaved output using reference liquid-audio model. + + Uses the same settings as the ONNX implementation for fair comparison. + """ + from liquid_audio import ChatState + + # Load audio + audio_tensor, sample_rate = load_audio_tensor(audio_path) + + # Build chat state + state = ChatState(processor, dtype=torch.float32) + state.new_turn("system") + state.add_text("You are a helpful assistant.") + state.end_turn() + state.new_turn("user") + state.add_audio(audio_tensor, sample_rate) + state.end_turn() + state.new_turn("assistant") + + # Generate with interleaved mode + text_tokens = [] + audio_codes = [] + + for token in model.generate_interleaved( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=max_new_tokens, + text_temperature=0.7, + audio_temperature=0.8, + ): + if token.numel() == 1: + token_id = token.item() + if token_id == 7: # <|im_end|> + break + if token_id == 130: # <|text_end|> + continue + text_tokens.append(token_id) + elif token.numel() == 8: + codes = token.cpu().numpy().flatten() + if np.any(codes >= 2048): + break + audio_codes.append(codes) + + text_output = processor.text.decode(text_tokens, skip_special_tokens=True) + return text_output, audio_codes + + +def test_interleaved_reference_audio_codes( + reference_model, + onnx_model, + sample_audio_short: pathlib.Path, +): + """Test that interleaved mode produces similar audio codes to reference. + + Note: Due to numerical differences between ONNX and PyTorch, exact match + is not expected. This test validates that both produce reasonable output. + """ + model, processor = reference_model + + logger.info(f"Testing interleaved reference: {sample_audio_short.name}") + + # Generate with reference + ref_text, ref_codes = generate_reference_interleaved( + model, processor, str(sample_audio_short), max_new_tokens=100 + ) + logger.info(f" Reference: {len(ref_text)} chars, {len(ref_codes)} audio frames") + + # Generate with ONNX + onnx_text, onnx_codes = onnx_model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=100, + text_temperature=0.7, + audio_temperature=0.8, + ) + logger.info(f" ONNX: {len(onnx_text)} chars, {len(onnx_codes)} audio frames") + + # Both should produce some output + assert len(ref_text) > 0 or len(ref_codes) > 0, "Reference produced no output" + assert len(onnx_text) > 0 or len(onnx_codes) > 0, "ONNX produced no output" + + # Log first few audio codes for comparison if both produced audio + if len(ref_codes) > 0 and len(onnx_codes) > 0: + logger.info(f" Reference first frame: {ref_codes[0].tolist()}") + logger.info(f" ONNX first frame: {onnx_codes[0].tolist()}") + + # Check code validity + for name, codes in [("Reference", ref_codes), ("ONNX", onnx_codes)]: + for i, frame in enumerate(codes): + assert frame.shape == (8,), f"{name} frame {i} wrong shape: {frame.shape}" + assert np.all(frame >= 0), f"{name} frame {i} has negative values" + assert np.all(frame < 2048), f"{name} frame {i} has values >= 2048" + + logger.info(" Both produced valid interleaved output") + + +def test_interleaved_reference_text_similarity( + reference_model, + onnx_model, + sample_audio_short: pathlib.Path, +): + """Test that interleaved text output is semantically similar. + + Uses deterministic settings (temperature=0) for more consistent comparison. + """ + from liquid_audio import ChatState + + model, processor = reference_model + + logger.info(f"Testing interleaved text similarity: {sample_audio_short.name}") + + # Load audio + audio_tensor, sample_rate = load_audio_tensor(str(sample_audio_short)) + + # Reference with deterministic settings + state = ChatState(processor, dtype=torch.float32) + state.new_turn("system") + state.add_text("You are a helpful assistant.") + state.end_turn() + state.new_turn("user") + state.add_audio(audio_tensor, sample_rate) + state.end_turn() + state.new_turn("assistant") + + ref_text_tokens = [] + for token in model.generate_interleaved( + text=state["text"], + audio_in=state["audio_in"], + audio_in_lens=state["audio_in_lens"], + audio_out=state["audio_out"], + modality_flag=state["modality_flag"], + max_new_tokens=50, + text_temperature=0, + audio_temperature=0, + ): + if token.numel() == 1: + token_id = token.item() + if token_id == 7: # <|im_end|> + break + if token_id == 130: # <|text_end|> + continue + ref_text_tokens.append(token_id) + + ref_text = processor.text.decode(ref_text_tokens, skip_special_tokens=True) + + # ONNX with deterministic settings + onnx_text, _ = onnx_model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=50, + text_temperature=0, + audio_temperature=0, + ) + + logger.info( + f" Reference text: {ref_text[:100]}..." + if len(ref_text) > 100 + else f" Reference text: {ref_text}" + ) + logger.info( + f" ONNX text: {onnx_text[:100]}..." + if len(onnx_text) > 100 + else f" ONNX text: {onnx_text}" + ) + + # Both should produce some text + assert len(ref_text) > 0, "Reference produced no text" + assert len(onnx_text) > 0, "ONNX produced no text" + + # Check if they share common words (loose similarity check) + ref_words = set(ref_text.lower().split()) + onnx_words = set(onnx_text.lower().split()) + common_words = ref_words & onnx_words + + logger.info(f" Common words: {len(common_words)} ({common_words})") + # At least some overlap expected for same audio input + # Note: This is a weak check since outputs can legitimately differ diff --git a/tests/test_lfm2_audio/test_samples.py b/tests/test_lfm2_audio/test_samples.py new file mode 100644 index 0000000..e585cb5 --- /dev/null +++ b/tests/test_lfm2_audio/test_samples.py @@ -0,0 +1,171 @@ +""" +Cross-precision consistency tests for LFM2.5-Audio ONNX exports. + +Tests that outputs are consistent across precisions (fp32, fp16, q4, q8). + +Run with: + uv run pytest tests/test_lfm2_audio/test_samples.py -v +""" + +import logging +import pathlib + +import pytest + +logger = logging.getLogger(__name__) + +# Expected transcription keywords for ASR validation +ASR_KEYWORDS = { + "fool_me_once_mono.wav": ["tennessee", "texas", "fool"], + "woodworks_question.wav": ["woodwork", "slogan", "tagline"], +} + + +def get_model_dir(exports_dir: pathlib.Path) -> pathlib.Path: + """Get the ONNX model directory.""" + return exports_dir / "LFM2.5-Audio-1.5B-ONNX" + + +def load_onnx_model(model_dir: pathlib.Path, precision: str | None): + """Load ONNX model with specified precision.""" + from liquidonnx.lfm2_audio.infer import LFM2AudioInference, resolve_precision_files + + files = resolve_precision_files(precision) + return LFM2AudioInference( + model_dir, + decoder_file=files["decoder"], + audio_embedding_file=files["audio_embedding"], + audio_encoder_file=files["audio_encoder"], + audio_detokenizer_file=files["audio_detokenizer"], + vocoder_depthformer_file=files["vocoder_depthformer"], + ) + + +def check_precision_available(model_dir: pathlib.Path, precision: str | None) -> bool: + """Check if the precision models are available.""" + onnx_dir = model_dir / "onnx" + if precision is None: + return (onnx_dir / "decoder.onnx").exists() + return (onnx_dir / f"decoder_{precision}.onnx").exists() + + +def test_asr_consistency_across_precisions( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, +): + """Test that ASR produces similar results across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + + results = {} + precisions = [None, "fp16", "q4", "q8"] + + for precision in precisions: + if not check_precision_available(model_dir, precision): + logger.info(f"Skipping {precision or 'fp32'} - not available") + continue + + model = load_onnx_model(model_dir, precision) + transcription = model.transcribe(str(sample_audio_short)) + results[precision or "fp32"] = transcription + del model + + if len(results) < 2: + pytest.skip("Need at least 2 precisions to compare") + + logger.info("ASR results across precisions:") + for prec, text in results.items(): + logger.info(f" {prec}: {text}") + + # All results should have the same keywords (allowing for minor variations) + audio_name = sample_audio_short.name + if audio_name in ASR_KEYWORDS: + for prec, text in results.items(): + text_lower = text.lower() + found = [kw for kw in ASR_KEYWORDS[audio_name] if kw in text_lower] + assert len(found) > 0, f"{prec} missing expected keywords" + + +def test_tts_consistency_across_precisions(exports_dir: pathlib.Path): + """Test that TTS produces audio of similar length across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + + prompt = "Hello world" + results = {} + precisions = [None, "fp16", "q4", "q8"] + + for precision in precisions: + if not check_precision_available(model_dir, precision): + logger.info(f"Skipping {precision or 'fp32'} - not available") + continue + + model = load_onnx_model(model_dir, precision) + audio_codes = model.synthesize( + text=prompt, + max_new_tokens=100, + audio_temperature=0, # Deterministic + text_temperature=0, + ) + results[precision or "fp32"] = len(audio_codes) + del model + + if len(results) < 2: + pytest.skip("Need at least 2 precisions to compare") + + logger.info("TTS frame counts across precisions:") + for prec, count in results.items(): + logger.info(f" {prec}: {count} frames") + + # Frame counts should be within 50% of each other (allowing for quantization effects) + counts = list(results.values()) + max_count = max(counts) + min_count = min(counts) + if max_count > 0: + ratio = min_count / max_count + assert ratio > 0.5, f"Frame count variation too high: {min_count} vs {max_count}" + + +def test_interleaved_consistency_across_precisions( + exports_dir: pathlib.Path, + sample_audio_short: pathlib.Path, +): + """Test that interleaved mode produces output across all precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + + results = {} + precisions = [None, "fp16", "q4", "q8"] + + for precision in precisions: + if not check_precision_available(model_dir, precision): + logger.info(f"Skipping {precision or 'fp32'} - not available") + continue + + model = load_onnx_model(model_dir, precision) + text_output, audio_codes = model.generate_interleaved_from_audio( + audio_path=str(sample_audio_short), + max_new_tokens=100, + audio_temperature=0.8, + text_temperature=0.7, + ) + results[precision or "fp32"] = { + "text_len": len(text_output), + "audio_frames": len(audio_codes), + } + del model + + if len(results) < 2: + pytest.skip("Need at least 2 precisions to compare") + + logger.info("Interleaved results across precisions:") + for prec, data in results.items(): + logger.info(f" {prec}: text={data['text_len']} chars, audio={data['audio_frames']} frames") + + # All precisions should produce some output + for prec, data in results.items(): + total = data["text_len"] + data["audio_frames"] + assert total > 0, f"{prec} produced no output" diff --git a/tests/test_lfm2_audio/test_tts.py b/tests/test_lfm2_audio/test_tts.py new file mode 100644 index 0000000..61988eb --- /dev/null +++ b/tests/test_lfm2_audio/test_tts.py @@ -0,0 +1,487 @@ +""" +TTS (Text-to-Speech) tests for LFM2.5-Audio ONNX exports. + +Tests audio generation quality across all precisions (fp32, fp16, q4, q8). +Includes reference comparison tests against PyTorch model. + +Run with: + uv run pytest tests/test_lfm2_audio/test_tts.py -v + uv run pytest tests/test_lfm2_audio/test_tts.py -v -k "fp16" + uv run pytest tests/test_lfm2_audio/test_tts.py -v -k "reference" +""" + +import logging +import pathlib + +import numpy as np +import pytest +import torch + +logger = logging.getLogger(__name__) + +# Precision configurations +PRECISION_CONFIGS = [ + pytest.param(None, id="fp32"), + pytest.param("fp16", id="fp16"), + pytest.param("q4", id="q4"), + pytest.param("q8", id="q8"), +] + +# Test prompts +TTS_PROMPTS = [ + pytest.param("Hello world", id="hello"), + pytest.param("The quick brown fox jumps over the lazy dog.", id="pangram"), +] + +# Short prompts for reference comparison (deterministic) +REFERENCE_PROMPTS = ["Hello world", "How are you today?", "The quick brown fox"] + + +def get_model_dir(exports_dir: pathlib.Path) -> pathlib.Path: + """Get the ONNX model directory.""" + return exports_dir / "LFM2.5-Audio-1.5B-ONNX" + + +def load_onnx_model(model_dir: pathlib.Path, precision: str | None): + """Load ONNX model with specified precision.""" + from liquidonnx.lfm2_audio.infer import LFM2AudioInference, resolve_precision_files + + files = resolve_precision_files(precision) + return LFM2AudioInference( + model_dir, + decoder_file=files["decoder"], + audio_embedding_file=files["audio_embedding"], + audio_encoder_file=files["audio_encoder"], + audio_detokenizer_file=files["audio_detokenizer"], + vocoder_depthformer_file=files["vocoder_depthformer"], + ) + + +def check_precision_available(model_dir: pathlib.Path, precision: str | None) -> bool: + """Check if the precision models are available.""" + onnx_dir = model_dir / "onnx" + if precision is None: + return (onnx_dir / "decoder.onnx").exists() + return (onnx_dir / f"decoder_{precision}.onnx").exists() + + +# === Precision-based Tests === + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +@pytest.mark.parametrize("prompt", TTS_PROMPTS) +def test_tts_generation( + exports_dir: pathlib.Path, + precision: str | None, + prompt: str, +): + """Test TTS audio generation across precisions.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + logger.info(f"Testing TTS ({precision or 'fp32'}): '{prompt}'") + + # Generate audio codes + audio_codes = model.synthesize( + text=prompt, + max_new_tokens=100, + audio_temperature=0.8, + text_temperature=0, + ) + + logger.info(f" Generated {len(audio_codes)} audio frames") + + # Validate output + assert len(audio_codes) > 0, "No audio frames generated" + + # Check code validity (should be in range [0, 2047]) + for i, frame in enumerate(audio_codes): + assert frame.shape == (8,), f"Frame {i} has wrong shape: {frame.shape}" + assert np.all(frame >= 0), f"Frame {i} has negative values" + assert np.all(frame < 2048), f"Frame {i} has values >= 2048" + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_tts_decoding(exports_dir: pathlib.Path, precision: str | None): + """Test TTS with full audio decoding (codes -> waveform).""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + prompt = "Hello" + logger.info(f"Testing TTS decoding ({precision or 'fp32'}): '{prompt}'") + + # Generate audio codes + audio_codes = model.synthesize( + text=prompt, + max_new_tokens=50, + audio_temperature=0.8, + text_temperature=0, + ) + + assert len(audio_codes) > 0, "No audio frames generated" + + # Decode to waveform + codes_array = np.stack(audio_codes, axis=0) # [T, 8] + waveform = model.decode_audio(codes_array) + + logger.info(f" Generated {len(audio_codes)} frames -> {len(waveform)} samples") + + # Validate waveform + assert len(waveform) > 0, "Empty waveform" + assert waveform.dtype == np.float32, f"Wrong dtype: {waveform.dtype}" + assert np.abs(waveform).max() <= 1.5, "Waveform values out of range" + + del model + + +@pytest.mark.parametrize("precision", PRECISION_CONFIGS) +def test_tts_deterministic(exports_dir: pathlib.Path, precision: str | None): + """Test that TTS with temperature=0 is deterministic.""" + model_dir = get_model_dir(exports_dir) + if not model_dir.exists(): + pytest.skip(f"Model not found: {model_dir}") + if not check_precision_available(model_dir, precision): + pytest.skip(f"Precision {precision or 'fp32'} not available") + + model = load_onnx_model(model_dir, precision) + + prompt = "Hello" + + # Generate twice with same settings + codes1 = model.synthesize( + text=prompt, max_new_tokens=30, audio_temperature=0, text_temperature=0 + ) + codes2 = model.synthesize( + text=prompt, max_new_tokens=30, audio_temperature=0, text_temperature=0 + ) + + assert len(codes1) == len(codes2), f"Frame count differs: {len(codes1)} vs {len(codes2)}" + + for i, (c1, c2) in enumerate(zip(codes1, codes2, strict=True)): + assert np.array_equal(c1, c2), f"Frame {i} differs between runs" + + logger.info(f" Deterministic: {len(codes1)} frames match") + + del model + + +# === Reference Comparison Tests (fp32 only) === + + +def generate_reference_tts(model, processor, text: str, max_new_tokens: int = 60): + """Generate TTS audio codes using reference model (greedy sampling).""" + from liquid_audio import ChatState + + state = ChatState(processor, dtype=torch.float32) + state.new_turn("system") + state.add_text("Perform TTS.") + state.end_turn() + state.new_turn("user") + state.add_text(text) + state.end_turn() + state.new_turn("assistant") + + audio_codes = [] + for token in model.generate_sequential( + **state, + max_new_tokens=max_new_tokens, + text_temperature=0, + audio_temperature=0, + ): + if token.shape != torch.Size([1]): + codes = token.cpu().numpy() + if np.any(codes >= 2048): + break + audio_codes.append(codes) + + return audio_codes + + +def generate_onnx_tts(model, text: str, max_new_tokens: int = 60): + """Generate TTS audio codes using ONNX model (greedy sampling).""" + audio_codes = model.synthesize( + text=text, + max_new_tokens=max_new_tokens, + audio_temperature=0, + text_temperature=0, + ) + # Filter out any end-of-audio frames + valid_codes = [c for c in audio_codes if np.all(c < 2048)] + return valid_codes + + +@pytest.mark.parametrize("prompt", REFERENCE_PROMPTS) +def test_tts_reference_single_turn(reference_model, onnx_model, prompt: str): + """Test single-turn TTS audio code generation against reference. + + Note: Due to numerical differences in ONNX vs PyTorch, exact match is not + expected. This test validates that both produce reasonable audio output. + """ + model, processor = reference_model + + logger.info(f"Testing TTS reference: '{prompt}'") + + # Generate with reference + ref_codes = generate_reference_tts(model, processor, prompt) + logger.info(f" Reference: {len(ref_codes)} frames") + + # Generate with ONNX + onnx_codes = generate_onnx_tts(onnx_model, prompt) + logger.info(f" ONNX: {len(onnx_codes)} frames") + + # Validate both produce audio + assert len(ref_codes) > 0, "Reference produced no audio frames" + assert len(onnx_codes) > 0, "ONNX produced no audio frames" + + # Check both produce at least a minimum amount of audio (5 frames = ~0.1s) + # Note: TTS output length can vary significantly between implementations + # due to different EOS detection, temperature handling, etc. + assert len(ref_codes) >= 5, f"Reference produced too few frames: {len(ref_codes)}" + assert len(onnx_codes) >= 5, f"ONNX produced too few frames: {len(onnx_codes)}" + + # Check code validity + for codes in [ref_codes, onnx_codes]: + for frame in codes: + assert np.all(frame >= 0), "Negative code values" + assert np.all(frame < 2048), "Code values out of range" + + logger.info(f" Both produced audio: ref={len(ref_codes)}, onnx={len(onnx_codes)} frames") + + +def test_tts_reference_multi_turn(reference_model, onnx_model): + """Test multi-turn TTS maintains context correctly. + + Note: Due to numerical differences between ONNX and PyTorch, we don't expect + exact match. This test validates that both models can generate audio across + multiple turns with proper context handling. + """ + from liquid_audio import ChatState + from liquid_audio.processor import LFMModality + + model, processor = reference_model + turns = ["Hello", "World"] + + logger.info(f"Testing multi-turn TTS reference: {turns}") + + # === Reference multi-turn === + state = ChatState(processor, dtype=torch.float32) + state.new_turn("system") + state.add_text("Perform TTS.") + state.end_turn() + + ref_all_codes = [] + for user_text in turns: + state.new_turn("user") + state.add_text(user_text) + state.end_turn() + state.new_turn("assistant") + + text_tokens = [] + audio_codes = [] + for token in model.generate_sequential( + **state, + max_new_tokens=50, + text_temperature=0, + audio_temperature=0, + ): + if token.shape == torch.Size([1]): + text_tokens.append(token) + else: + codes = token.cpu().numpy() + if np.any(codes >= 2048): + break + audio_codes.append(token) + + ref_all_codes.append([c.cpu().numpy() for c in audio_codes]) + + # Update state + if text_tokens: + text_tensor = torch.cat(text_tokens, dim=0).unsqueeze(0) + else: + text_tensor = torch.empty((1, 0), dtype=torch.long) + + if audio_codes: + audio_tensor = torch.stack(audio_codes, dim=1) + else: + audio_tensor = torch.empty((8, 0), dtype=torch.long) + + mod_text = torch.full((1, text_tensor.shape[1]), LFMModality.TEXT, dtype=torch.long) + mod_audio = torch.full((1, audio_tensor.shape[1]), LFMModality.AUDIO_OUT, dtype=torch.long) + mod_flag = torch.cat([mod_text, mod_audio], dim=1) + + state.append(text_tensor, audio_tensor, mod_flag) + state.end_turn() + + # === ONNX multi-turn === + cache = onnx_model._init_cache(batch_size=1) + total_len = 0 + + prompt_parts = [ + "<|startoftext|><|im_start|>system\n", + "Perform TTS.<|im_end|>\n", + ] + + onnx_all_codes = [] + for turn_idx, user_text in enumerate(turns): + turn_prompt = f"<|im_start|>user\n{user_text}<|im_end|>\n<|im_start|>assistant\n" + + if turn_idx == 0: + full_prompt = "".join(prompt_parts) + turn_prompt + else: + full_prompt = turn_prompt + + input_ids = onnx_model.tokenizer.encode( + full_prompt, return_tensors="np", add_special_tokens=False + ) + batch_size, seq_len = input_ids.shape + embeds = onnx_model._get_text_embeds(input_ids) + + attention_mask = np.ones((batch_size, total_len + seq_len), dtype=np.int64) + logits, hidden_states, cache = onnx_model._run_decoder(embeds, attention_mask, cache) + total_len += seq_len + + # Generate text until audio_start + in_audio_mode = False + for _ in range(10): + last_logits = logits[0, -1, : onnx_model.vocab_size] + token = int(np.argmax(last_logits)) + + if token == onnx_model.AUDIO_START_TOKEN: + in_audio_mode = True + next_ids = np.array([[onnx_model.AUDIO_START_TOKEN]], dtype=np.int64) + next_embeds = onnx_model._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = onnx_model._run_decoder( + next_embeds, attention_mask, cache + ) + total_len += 1 + break + + next_ids = np.array([[token]], dtype=np.int64) + next_embeds = onnx_model._get_text_embeds(next_ids) + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = onnx_model._run_decoder( + next_embeds, attention_mask, cache + ) + total_len += 1 + + assert in_audio_mode, f"Turn {turn_idx}: Did not enter audio mode" + + # Generate audio + audio_codes = [] + for _ in range(50): + last_hidden = hidden_states[0, -1:, :] + frame_codes = onnx_model._sample_audio_codes(last_hidden, temperature=0) + + if onnx_model._is_end_of_audio(frame_codes[0]): + break + + audio_codes.append(frame_codes[0]) + + clamped_codes = np.minimum(frame_codes[0], 2047) + audio_tokens = np.array( + [ + [ + cb_idx * onnx_model.codebook_vocab + int(clamped_codes[cb_idx]) + for cb_idx in range(onnx_model.num_codebooks) + ] + ], + dtype=np.int64, + ) + all_embeds = onnx_model._get_audio_embeds(audio_tokens) + next_embeds = all_embeds.sum(axis=1, keepdims=True) + + attention_mask = np.ones((batch_size, total_len + 1), dtype=np.int64) + logits, hidden_states, cache = onnx_model._run_decoder( + next_embeds, attention_mask, cache + ) + total_len += 1 + + onnx_all_codes.append(audio_codes) + + # End turn + end_ids = onnx_model.tokenizer.encode( + "<|im_end|>\n", return_tensors="np", add_special_tokens=False + ) + end_embeds = onnx_model._get_text_embeds(end_ids) + attention_mask = np.ones((batch_size, total_len + end_ids.shape[1]), dtype=np.int64) + logits, hidden_states, cache = onnx_model._run_decoder(end_embeds, attention_mask, cache) + total_len += end_ids.shape[1] + + # Compare - validate both produce audio in each turn + assert len(ref_all_codes) == len(onnx_all_codes) + + for turn_idx in range(len(turns)): + ref_codes = ref_all_codes[turn_idx] + onnx_codes = onnx_all_codes[turn_idx] + + logger.info( + f" Turn {turn_idx + 1} '{turns[turn_idx]}': ref={len(ref_codes)}, onnx={len(onnx_codes)}" + ) + + # Both should produce some audio (allow empty for very short inputs) + # Just validate code ranges + for codes in [ref_codes, onnx_codes]: + for frame in codes: + assert np.all(frame >= 0), "Negative code values" + assert np.all(frame < 2048), "Code values out of range" + + logger.info(" Multi-turn generation completed for both models") + + +def test_tts_reference_audio_decoding(reference_model, onnx_model, audio_processor): + """Test that both models can generate audio that decodes to valid waveforms. + + Note: Due to numerical differences, we don't expect identical codes or waveforms. + This test validates that both models produce decodable audio. + """ + model, processor = reference_model + text = "Hello world" + + # Generate codes + ref_codes = generate_reference_tts(model, processor, text) + onnx_codes = generate_onnx_tts(onnx_model, text) + + assert len(ref_codes) > 0, "Reference produced no audio codes" + assert len(onnx_codes) > 0, "ONNX produced no audio codes" + + # Decode both + device = "cpu" + + ref_array = np.stack(ref_codes, axis=0) + ref_tensor = torch.from_numpy(ref_array.T).unsqueeze(0).long().to(device) + + onnx_array = np.stack(onnx_codes, axis=0) + onnx_tensor = torch.from_numpy(onnx_array.T).unsqueeze(0).long().to(device) + + with torch.no_grad(): + ref_wav = audio_processor.decode(ref_tensor) + onnx_wav = audio_processor.decode(onnx_tensor) + + # Validate waveforms + ref_np = ref_wav.squeeze().cpu().numpy() + onnx_np = onnx_wav.squeeze().cpu().numpy() + + # Both should produce valid audio + assert len(ref_np) > 0, "Reference waveform is empty" + assert len(onnx_np) > 0, "ONNX waveform is empty" + assert np.abs(ref_np).max() <= 1.5, "Reference waveform out of range" + assert np.abs(onnx_np).max() <= 1.5, "ONNX waveform out of range" + + ref_rms = np.sqrt(np.mean(ref_np**2)) + onnx_rms = np.sqrt(np.mean(onnx_np**2)) + + logger.info(f" Reference: {len(ref_codes)} frames, {len(ref_np)} samples, RMS={ref_rms:.4f}") + logger.info(f" ONNX: {len(onnx_codes)} frames, {len(onnx_np)} samples, RMS={onnx_rms:.4f}") diff --git a/uv.lock b/uv.lock index fab4e31..0b42f3b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,13 +1,27 @@ version = 1 revision = 3 -requires-python = ">=3.11" +requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x'", - "python_full_version == '3.12.*' and platform_machine != 's390x'", - "python_full_version < '3.12' and platform_machine != 's390x'", - "python_full_version >= '3.13' and platform_machine == 's390x'", - "python_full_version == '3.12.*' and platform_machine == 's390x'", - "python_full_version < '3.12' and platform_machine == 's390x'", + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, ] [[package]] @@ -23,6 +37,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -32,28 +115,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, @@ -138,6 +262,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "einops" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, +] + [[package]] name = "filelock" version = "3.20.1" @@ -293,6 +435,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pooch" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "soxr" }, + { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, + { name = "standard-sunau", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "liquid-audio" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "einops" }, + { name = "librosa" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "torchcodec" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/56/f899e9c29481209027d9514438caace608cf0ce74cd85ca28b68dfcfd70e/liquid_audio-1.1.0.tar.gz", hash = "sha256:57d633ce65c55e050bbef0671c7b8fad5cf8b75e3cc9e0d36fec38d7a04221c7", size = 135558, upload-time = "2026-01-06T01:59:21.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/52/b69b3c9c0a4853c1f9a9670dd22c3aea708e26da857342feca8fd922d238/liquid_audio-1.1.0-py3-none-any.whl", hash = "sha256:4c876dd2b481664e2464568c93eabf5447511db1f8a9200294fd19add3e36097", size = 165833, upload-time = "2026-01-06T01:59:20.26Z" }, +] + [[package]] name = "liquidonnx" version = "0.1.0" @@ -302,7 +510,9 @@ dependencies = [ { name = "onnx" }, { name = "onnx-ir" }, { name = "onnxruntime" }, + { name = "onnxscript" }, { name = "pillow" }, + { name = "scipy" }, { name = "torch" }, { name = "torchvision" }, { name = "transformers" }, @@ -310,6 +520,7 @@ dependencies = [ [package.optional-dependencies] dev = [ + { name = "liquid-audio" }, { name = "pytest" }, { name = "ruff" }, ] @@ -317,39 +528,65 @@ gpu = [ { name = "onnxruntime-gpu" }, ] +[package.dev-dependencies] +dev = [ + { name = "liquid-audio" }, + { name = "pytest" }, + { name = "ruff" }, +] + [package.metadata] requires-dist = [ - { name = "numpy", specifier = ">=2.2.0" }, + { name = "liquid-audio", marker = "extra == 'dev'", specifier = ">=1.1.0" }, + { name = "numpy", specifier = ">=2.2.0,<2.4" }, { name = "onnx" }, { name = "onnx-ir", specifier = ">=0.1.13" }, { name = "onnxruntime", specifier = ">=1.24.0.dev0", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/pypi/simple/" }, { name = "onnxruntime-gpu", marker = "extra == 'gpu'", specifier = ">=1.24.0.dev0", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ORT-Nightly/pypi/simple/" }, + { name = "onnxscript", specifier = ">=0.5.7" }, { name = "pillow" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'" }, + { name = "scipy", specifier = ">=1.12.0" }, { name = "torch", specifier = ">=2.0.0" }, { name = "torchvision", specifier = ">=0.24.1" }, - { name = "transformers", git = "ssh://git@github.com/Liquid4All/transformers_private.git?branch=lfm2-config" }, + { name = "transformers", git = "https://github.com/huggingface/transformers.git?rev=3c25177" }, ] provides-extras = ["gpu", "dev"] +[package.metadata.requires-dev] +dev = [ + { name = "liquid-audio", specifier = ">=1.1.0" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.14.10" }, +] + +[[package]] +name = "llvmlite" +version = "0.46.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, + { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, + { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -416,11 +653,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, @@ -457,6 +689,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msgpack" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +] + [[package]] name = "networkx" version = "3.6.1" @@ -466,83 +742,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "numba" +version = "0.63.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llvmlite" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, + { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" }, + { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" }, +] + [[package]] name = "numpy" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166, upload-time = "2025-12-20T16:15:43.434Z" }, - { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781, upload-time = "2025-12-20T16:15:45.701Z" }, - { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247, upload-time = "2025-12-20T16:15:47.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807, upload-time = "2025-12-20T16:15:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992, upload-time = "2025-12-20T16:15:51.615Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871, upload-time = "2025-12-20T16:15:54.129Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190, upload-time = "2025-12-20T16:15:56.147Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762, upload-time = "2025-12-20T16:15:58.524Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359, upload-time = "2025-12-20T16:16:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132, upload-time = "2025-12-20T16:16:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977, upload-time = "2025-12-20T16:16:04.77Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, - { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, - { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" }, - { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" }, - { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" }, - { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, - { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088, upload-time = "2025-12-20T16:16:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065, upload-time = "2025-12-20T16:16:33.491Z" }, - { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640, upload-time = "2025-12-20T16:16:35.636Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556, upload-time = "2025-12-20T16:16:37.276Z" }, - { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562, upload-time = "2025-12-20T16:16:38.953Z" }, - { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719, upload-time = "2025-12-20T16:16:41.503Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053, upload-time = "2025-12-20T16:16:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859, upload-time = "2025-12-20T16:16:47.174Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849, upload-time = "2025-12-20T16:16:49.554Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840, upload-time = "2025-12-20T16:16:51.227Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509, upload-time = "2025-12-20T16:16:53.313Z" }, - { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815, upload-time = "2025-12-20T16:16:55.496Z" }, - { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321, upload-time = "2025-12-20T16:16:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635, upload-time = "2025-12-20T16:16:59.379Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053, upload-time = "2025-12-20T16:17:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702, upload-time = "2025-12-20T16:17:04.235Z" }, - { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493, upload-time = "2025-12-20T16:17:06.856Z" }, - { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222, upload-time = "2025-12-20T16:17:09.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" }, - { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, - { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" }, - { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" }, - { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" }, - { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" }, - { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" }, - { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" }, - { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947, upload-time = "2025-12-20T16:18:09.836Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119, upload-time = "2025-12-20T16:18:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815, upload-time = "2025-12-20T16:18:14.433Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376, upload-time = "2025-12-20T16:18:16.524Z" }, +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] [[package]] @@ -582,7 +866,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -593,7 +877,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -620,9 +904,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "platform_machine != 's390x'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine != 's390x'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -633,7 +917,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 's390x'" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -691,12 +975,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/bd/bf/824b13b7ea14c2d374b48a296cfa412442e5559326fbab5441a4fcb68924/onnx-1.20.0.tar.gz", hash = "sha256:1a93ec69996b4556062d552ed1aa0671978cfd3c17a40bf4c89a1ae169c6a4ad", size = 12049527, upload-time = "2025-12-01T18:14:34.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/9a/125ad5ed919d1782b26b0b4404e51adc44afd029be30d5a81b446dccd9c5/onnx-1.20.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:00dc8ae2c7b283f79623961f450b5515bd2c4b47a7027e7a1374ba49cef27768", size = 18341929, upload-time = "2025-12-01T18:13:43.79Z" }, - { url = "https://files.pythonhosted.org/packages/4d/3c/85280dd05396493f3e1b4feb7a3426715e344b36083229437f31d9788a01/onnx-1.20.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f62978ecfb8f320faba6704abd20253a5a79aacc4e5d39a9c061dd63d3b7574f", size = 17899362, upload-time = "2025-12-01T18:13:46.496Z" }, - { url = "https://files.pythonhosted.org/packages/26/db/e11cf9aaa6ccbcd27ea94d321020fef3207cba388bff96111e6431f97d1a/onnx-1.20.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:71177f8fd5c0dd90697bc281f5035f73707bdac83257a5c54d74403a1100ace9", size = 18119129, upload-time = "2025-12-01T18:13:49.662Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0b/1b99e7ba5ccfa8ecb3509ec579c8520098d09b903ccd520026d60faa7c75/onnx-1.20.0-cp311-cp311-win32.whl", hash = "sha256:1d3d0308e2c194f4b782f51e78461b567fac8ce6871c0cf5452ede261683cc8f", size = 16364604, upload-time = "2025-12-01T18:13:52.691Z" }, - { url = "https://files.pythonhosted.org/packages/51/ab/7399817821d0d18ff67292ac183383e41f4f4ddff2047902f1b7b51d2d40/onnx-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a6de7dda77926c323b0e5a830dc9c2866ce350c1901229e193be1003a076c25", size = 16488019, upload-time = "2025-12-01T18:13:55.776Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/23059c11d9c0fb1951acec504a5cc86e1dd03d2eef3a98cf1941839f5322/onnx-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:afc4cf83ce5d547ebfbb276dae8eb0ec836254a8698d462b4ba5f51e717fd1ae", size = 16446841, upload-time = "2025-12-01T18:13:58.091Z" }, { url = "https://files.pythonhosted.org/packages/5e/19/2caa972a31014a8cb4525f715f2a75d93caef9d4b9da2809cc05d0489e43/onnx-1.20.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:31efe37d7d1d659091f34ddd6a31780334acf7c624176832db9a0a8ececa8fb5", size = 18340913, upload-time = "2025-12-01T18:14:00.477Z" }, { url = "https://files.pythonhosted.org/packages/78/bb/b98732309f2f6beb4cdcf7b955d7bbfd75a191185370ee21233373db381e/onnx-1.20.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d75da05e743eb9a11ff155a775cae5745e71f1cd0ca26402881b8f20e8d6e449", size = 17896118, upload-time = "2025-12-01T18:14:03.239Z" }, { url = "https://files.pythonhosted.org/packages/84/a7/38aa564871d062c11538d65c575af9c7e057be880c09ecbd899dd1abfa83/onnx-1.20.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02e0d72ab09a983fce46686b155a5049898558d9f3bc6e8515120d6c40666318", size = 18115415, upload-time = "2025-12-01T18:14:06.261Z" }, @@ -738,10 +1016,6 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:57607bb17617f726748219830576c9b7c84520c2aa62f8532e227134f8bef2ce" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a3023843787441c0b9a91b7b523e9ea91ba41913c5a7f0e4e14a0c9655d11dd" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f84c6379dc07c74253bfc38719f3e5967b58e89434a635635b628f53add7103" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp311-cp311-win_amd64.whl", hash = "sha256:33ff84fec7e1ebb2dadff70571c906d4a437aa0f8108bf3252312abc6144fd18" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:402f3a137c01e200c123a118e82837b452a71b082302ebb3cf9eab34aa97ae84" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2628c958c6dc7a5513c07594297482e087fd6c25256dfe1fd0a08653cbea09b1" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime/1.24.dev20251231004/onnxruntime-1.24.0.dev20251231004-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a53d561672fb8a9bfcc6695c1f0c1de418037509d4c5113f94691e9cda5d0ee7" }, @@ -771,8 +1045,6 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ef453af44ef3b7f8035fc9683512e0b5415e2395ce584cd4bd2f81bb528fa5b" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp311-cp311-win_amd64.whl", hash = "sha256:bf43cf0a72edfbceb1a5b02fccf82689ef15284710c29aa64e470f504fa69214" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67fa8dc964037bbf6acd8e1fff4b902d7165719c5862f69224be0fc35f34f89a" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp312-cp312-win_amd64.whl", hash = "sha256:2fbd84c26a352ebab5621daa7681fb58c64d48b1b079a7e8e1bc2c4693b72584" }, { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99e30ad7114fc94e74a50d06c700add311074eca8008274822c8d55cdfea6743" }, @@ -783,6 +1055,23 @@ wheels = [ { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/7982ae20-ed19-4a35-a362-a96ac99897b7/pypi/download/onnxruntime-gpu/1.24.dev20251127001/onnxruntime_gpu-1.24.0.dev20251127001-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7442af3541a5463961f9949e205ab1860a42432cd3b08efb3d657c0ea789ef4" }, ] +[[package]] +name = "onnxscript" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "onnx" }, + { name = "onnx-ir" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/f8/358a7d982ea51bc1b0c264f29c08adf096c62ba9f258ba13c954b41c46f5/onnxscript-0.5.7.tar.gz", hash = "sha256:480d572451bc233ed7f742b5005cb0c899594b2fdc28e15167dab26f7fd777ad", size = 596306, upload-time = "2025-12-16T20:47:15.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/ec/1656ea93be1e50baf429c20603dce249fa3571f3a180407cee79b1afa013/onnxscript-0.5.7-py3-none-any.whl", hash = "sha256:f94a66059c56d13b44908e9b7fd9dae4b4faa6681c784f3fd4c29cfa863e454e", size = 693353, upload-time = "2025-12-16T20:47:17.897Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -798,17 +1087,6 @@ version = "12.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, - { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, - { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, - { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, - { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, @@ -870,13 +1148,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, - { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, - { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] @@ -888,6 +1168,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pooch" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/77/b3d3e00c696c16cf99af81ef7b1f5fe73bd2a307abca41bd7605429fe6e5/pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10", size = 59353, upload-time = "2024-06-06T16:53:46.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/87/77cc11c7a9ea9fd05503def69e3d18605852cd0d4b0d3b8f15bbeb3ef1d1/pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47", size = 64574, upload-time = "2024-06-06T16:53:44.343Z" }, +] + [[package]] name = "protobuf" version = "6.33.2" @@ -903,6 +1197,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, ] +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -943,15 +1274,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -998,20 +1320,6 @@ version = "2025.11.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, - { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, - { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, - { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, @@ -1147,6 +1455,159 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, ] +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -1165,6 +1626,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "soxr" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f", size = 205508, upload-time = "2025-09-07T13:22:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4", size = 163568, upload-time = "2025-09-07T13:22:03.558Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc", size = 204707, upload-time = "2025-09-07T13:22:05.125Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b", size = 238032, upload-time = "2025-09-07T13:22:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2", size = 172070, upload-time = "2025-09-07T13:22:07.62Z" }, + { url = "https://files.pythonhosted.org/packages/99/77/d3b3c25b4f1b1aa4a73f669355edcaee7a52179d0c50407697200a0e55b9/soxr-1.0.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:392a5c70c04eb939c9c176bd6f654dec9a0eaa9ba33d8f1024ed63cf68cdba0a", size = 209509, upload-time = "2025-09-07T13:22:08.773Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ee/3ca73e18781bb2aff92b809f1c17c356dfb9a1870652004bd432e79afbfa/soxr-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fdc41a1027ba46777186f26a8fba7893be913383414135577522da2fcc684490", size = 167690, upload-time = "2025-09-07T13:22:10.259Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f0/eea8b5f587a2531657dc5081d2543a5a845f271a3bea1c0fdee5cebde021/soxr-1.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:449acd1dfaf10f0ce6dfd75c7e2ef984890df94008765a6742dafb42061c1a24", size = 209541, upload-time = "2025-09-07T13:22:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/64/59/2430a48c705565eb09e78346950b586f253a11bd5313426ced3ecd9b0feb/soxr-1.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38b35c99e408b8f440c9376a5e1dd48014857cd977c117bdaa4304865ae0edd0", size = 243025, upload-time = "2025-09-07T13:22:12.877Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1b/f84a2570a74094e921bbad5450b2a22a85d58585916e131d9b98029c3e69/soxr-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a39b519acca2364aa726b24a6fd55acf29e4c8909102e0b858c23013c38328e5", size = 184850, upload-time = "2025-09-07T13:22:14.068Z" }, +] + +[[package]] +name = "standard-aifc" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, +] + +[[package]] +name = "standard-chunk" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, +] + +[[package]] +name = "standard-sunau" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/ae/e3707f6c1bc6f7aa0df600ba8075bfb8a19252140cd595335be60e25f9ee/standard_sunau-3.13.0-py3-none-any.whl", hash = "sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622", size = 7364, upload-time = "2024-10-30T16:01:28.003Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1177,6 +1712,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "tokenizers" version = "0.22.1" @@ -1226,16 +1770,12 @@ dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "setuptools" }, { name = "sympy" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430, upload-time = "2025-11-12T15:20:31.705Z" }, - { url = "https://files.pythonhosted.org/packages/56/be/76eaa36c9cd032d3b01b001e2c5a05943df75f26211f68fae79e62f87734/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446, upload-time = "2025-11-12T15:20:15.544Z" }, - { url = "https://files.pythonhosted.org/packages/47/cc/7a2949e38dfe3244c4df21f0e1c27bce8aedd6c604a587dd44fc21017cb4/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074, upload-time = "2025-11-12T15:21:39.958Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ce/7d251155a783fb2c1bb6837b2b7023c622a2070a0a72726ca1df47e7ea34/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887, upload-time = "2025-11-12T15:20:36.611Z" }, { url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592, upload-time = "2025-11-12T15:20:41.62Z" }, { url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281, upload-time = "2025-11-12T15:22:17.602Z" }, { url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568, upload-time = "2025-11-12T15:21:18.689Z" }, @@ -1258,6 +1798,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, ] +[[package]] +name = "torchaudio" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/83/71cbadd7b66753818b5775f2088bad4f721d581de276996df4968000a626/torchaudio-2.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7581ef170794c599aed55918e00d0acd9e5c9a0f19400c9a9a840955180365c5", size = 808098, upload-time = "2025-11-12T15:26:01.408Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/32e8bec360459107f9b451cc1a5b6fdd5f1d3e653e65a111502084f21e3a/torchaudio-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:742f9d24db5f1f46d8c7e29c599fe55b866d92c4a8181fcb95eab12da225ceb0", size = 474604, upload-time = "2025-11-12T15:25:49.122Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0d/b5af1d55ede1ca07769a2cf71256073d8958e2a5521fc734fc19f5343283/torchaudio-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4533fdafba73d7bcfcb5f1225b2cc8974a290ed0fe54c44638d6f440e91b8999", size = 2059899, upload-time = "2025-11-12T15:26:19.363Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7c/df90eb0b337cbad59296ed91778e32be069330f5186256d4ce9ea603d324/torchaudio-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:923dccc67be4a6cbb45c3dcc2d69ee182bda75b09b69bc88cd3bcdfc739883a2", size = 665337, upload-time = "2025-11-12T15:26:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/3321ad6379ac2d968064704e8d015c31ccae5d1ece070f87fb44b17d90e6/torchaudio-2.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:bb69557484c92513a980027ec4cb314b0f43cf4442bbfd97440e66528dbad22d", size = 808136, upload-time = "2025-11-12T15:26:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/e2/fe55b3882157fd57aa131f5bcad90f0329be90827e1c0e0c482662ddef38/torchaudio-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ba2799ceec5e4373a0aa26df30d608f1eaaefd8ac4a7ae0c3446f63106f5b5a5", size = 474349, upload-time = "2025-11-12T15:26:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/74/d3/0b090c03cac5a20691507e0945589a696fb10402ccd2457eea47dbf8a71b/torchaudio-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bc3c8e9a240bfad8bc61f769324a4f3ce5d60eec161369d457c595c35dbb10c7", size = 2060343, upload-time = "2025-11-12T15:26:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/a0/db/2555cfd428f4bf09a4df1c6f9204d0acc217c46edb35776c16e7a2a9a1c9/torchaudio-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:13ee96ea9bbbc85e198cb671273af06f010e6981d7b912d001eef6bc74e23f4f", size = 665301, upload-time = "2025-11-12T15:26:04.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/e82d8b5f447abdddc950965f1395f36baef3602643dd069100c6369ba73e/torchaudio-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9290f6a6409deb1f9113d5aef97ec646eeee6410b6bcc57ab8b57066b54da7c1", size = 813456, upload-time = "2025-11-12T15:26:13.963Z" }, + { url = "https://files.pythonhosted.org/packages/ce/45/dd9ad6af9bb595095cd98028d270f933760968b92a3497282e31289ef3b4/torchaudio-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:eeae7ca60b64c4bfb78fbd104a089d072b151423d5d2f90da1da00787f03b800", size = 476577, upload-time = "2025-11-12T15:26:09.54Z" }, + { url = "https://files.pythonhosted.org/packages/79/97/c49aeb01d8a9ced2b8215a38b69b8eafd1afe295a487a73b7030c6ff3396/torchaudio-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:5f445e896215e6f7bba497dc68aab1e6cb077ae0ab3a90095067f16df6a9bb98", size = 2062158, upload-time = "2025-11-12T15:26:10.487Z" }, + { url = "https://files.pythonhosted.org/packages/ba/70/30b2a0ecca2a0a5e6a8cee8952fdea3872854ea5bcd86fe3df369fdc2543/torchaudio-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c558ba70d548f7491245ed7a35310f6310d83fc7591f073ab5fed9fd38cef987", size = 669253, upload-time = "2025-11-12T15:26:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/5b/38/0dabf362f946ab5773d3db3322718d652d70ad12a82f500d54c6c8b9cc88/torchaudio-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:69a582650279ee16ff9087f99b4234fe5d766e1bf7f0be352db5f46991854c1e", size = 810496, upload-time = "2025-11-12T15:26:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/05/1c/e05a32ee6868dc05463242db672f23dba5d042423fefcf294db4dac343a8/torchaudio-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9c0d004f784c49078017f8217fdc901df0eb9724e50fb269b3a6c99b1d4eae75", size = 474566, upload-time = "2025-11-12T15:26:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/15/52/8cec1fe90f05b888f9060467e1eb8c27f9295b8729a83d443e3bd7c471d3/torchaudio-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d2743b28ff5538d5fdf2ff6657d392852ccdfe640ede46f566b2907ca32d8dca", size = 2060358, upload-time = "2025-11-12T15:26:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/04/73/6ba396813d714f895f86c82be61b590fbe14255ebe6866f5ea5916c075a3/torchaudio-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:234c7a9d4d0a6ed735cd37965baa9a89ca36bdbebece8a6a5ff7727acbb43026", size = 665039, upload-time = "2025-11-12T15:26:18.308Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f6/237e00a04dea497a40a8567d024dfb39193abec3ca3695ad51919ad633d1/torchaudio-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e13cb38971ac259fc4e102282a3e48f6df5f0ab00eb785ca5155e3392d1e86f1", size = 813463, upload-time = "2025-11-12T15:26:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/5fcd46a80086030899badeb5a934fab337c88325b3f68c60faa0b672d4d2/torchaudio-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:35c96ed1011b50eaf17948da173b09450cdc5bb7f908687571adb4a4c072c05e", size = 476577, upload-time = "2025-11-12T15:26:17.355Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4c/bc428f71d5ef728fba2ecb151a3a6d187e6f0b9446b76e4f87e46d2206a3/torchaudio-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c220c4acf9914cce2dc81c3624d7c84008ef436dc31bcbb89e8f4416d3615a34", size = 2062170, upload-time = "2025-11-12T15:26:20.837Z" }, + { url = "https://files.pythonhosted.org/packages/07/0e/be41f412e1225bdbd9b7fd7f41a20f070c707f5274b82542eeccf6dc2b79/torchaudio-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cfd12934c7b54b41d4c79dfd26fbfe88fafa9cc5cc77c074e953bb7018d9322c", size = 669265, upload-time = "2025-11-12T15:26:14.976Z" }, +] + +[[package]] +name = "torchcodec" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/60/3bfa459e09987af08e188811b191437c9d8215a74f4d418be6ff7df87b5c/torchcodec-0.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8996ec62b72c69545c30246df64df386d06d7ec7de0689be5d20dfc06aad6442", size = 4064264, upload-time = "2025-12-10T15:55:56.313Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/bfb74babec98aff11ab4f239b0901f39e1a93338b3438e842d864dc46935/torchcodec-0.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a50568ce73b70395d113833fb07394c223f5546ef5d4fafe0fdcd91627fca270", size = 2061978, upload-time = "2025-12-10T15:55:33.415Z" }, + { url = "https://files.pythonhosted.org/packages/b2/12/c0bbf01b0ed52b69aaeed4af1043dc8308ccc522a47fcc082b34882e2ba2/torchcodec-0.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9c8efe5845bde45a428f96493b4a041511f47f5bd53b333a0ad90426be4623a", size = 2187178, upload-time = "2025-12-10T15:56:16.968Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/67fc8417f9efa8a25c00a44f0d674761a0bad9c45e9725e3fd116b3c48ed/torchcodec-0.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c9cdcba50c75be70ef6ec919ec1f7f14d9d5163d93cf6bd94403e134f03734c", size = 4034415, upload-time = "2025-12-10T15:56:02.04Z" }, + { url = "https://files.pythonhosted.org/packages/68/05/06240f661e9aa08b20765305e3b88f60bff706bbe54ac35830af74612443/torchcodec-0.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0643c5e9c3a51fdafdea87935d5b0a38e99626c664f47a150482d77ab370a877", size = 2067767, upload-time = "2025-12-10T15:55:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/13/a2/d78cd65863fb805d9e35fe90ae7574eab86ff0ae63438208bd07d2cf1fd2/torchcodec-0.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:df0b5a15998fd7457625c2af2a6276e0e710fac158d145045340dbbcd1cfdb65", size = 2186788, upload-time = "2025-12-10T15:56:20.204Z" }, + { url = "https://files.pythonhosted.org/packages/01/02/f8ae9443d3bcbe8a8d6d0bbc3992296e5476e5afa1f244100a3a7967a36c/torchcodec-0.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b9bc5a5dff925df96d11bf90bd0ce964b8086bb11ae09adf353518192b5da483", size = 3812248, upload-time = "2025-12-10T15:56:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/8462b55571286847ea31edb7634583125400824267db9ba8301f4ce3f137/torchcodec-0.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:65634bb28b3155cf99f980dac31ecedb414c07b8156f8473ec9fb74bedbd2a1f", size = 2068456, upload-time = "2025-12-10T15:55:40.577Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/752d0fc1c6e8f799ae880ca1087510def663a7f9aa1a70074ae334c6908f/torchcodec-0.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:2d01c8b3685a3a38f050ed2b526808a2938dba6f56cb9f9e967884fd858bba15", size = 2188320, upload-time = "2025-12-10T15:56:24.63Z" }, +] + [[package]] name = "torchvision" version = "0.24.1" @@ -1268,10 +1854,6 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/69/30f5f03752aa1a7c23931d2519b31e557f3f10af5089d787cddf3b903ecf/torchvision-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600", size = 1891436, upload-time = "2025-11-12T15:25:04.3Z" }, - { url = "https://files.pythonhosted.org/packages/0c/69/49aae86edb75fe16460b59a191fcc0f568c2378f780bb063850db0fe007a/torchvision-0.24.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1e39619de698e2821d71976c92c8a9e50cdfd1e993507dfb340f2688bfdd8283", size = 2387757, upload-time = "2025-11-12T15:25:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/1dfc3db98797b326f1d0c3f3bb61c83b167a813fc7eab6fcd2edb8c7eb9d/torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a0f106663e60332aa4fcb1ca2159ef8c3f2ed266b0e6df88de261048a840e0df", size = 8047682, upload-time = "2025-11-12T15:25:21.125Z" }, - { url = "https://files.pythonhosted.org/packages/fa/bb/cfc6a6f6ccc84a534ed1fdf029ae5716dd6ff04e57ed9dc2dab38bf652d5/torchvision-0.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:a9308cdd37d8a42e14a3e7fd9d271830c7fecb150dd929b642f3c1460514599a", size = 4037588, upload-time = "2025-11-12T15:25:14.402Z" }, { url = "https://files.pythonhosted.org/packages/f0/af/18e2c6b9538a045f60718a0c5a058908ccb24f88fde8e6f0fc12d5ff7bd3/torchvision-0.24.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e48bf6a8ec95872eb45763f06499f87bd2fb246b9b96cb00aae260fda2f96193", size = 1891433, upload-time = "2025-11-12T15:25:03.232Z" }, { url = "https://files.pythonhosted.org/packages/9d/43/600e5cfb0643d10d633124f5982d7abc2170dfd7ce985584ff16edab3e76/torchvision-0.24.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7fb7590c737ebe3e1c077ad60c0e5e2e56bb26e7bccc3b9d04dbfc34fd09f050", size = 2386737, upload-time = "2025-11-12T15:25:08.288Z" }, { url = "https://files.pythonhosted.org/packages/93/b1/db2941526ecddd84884132e2742a55c9311296a6a38627f9e2627f5ac889/torchvision-0.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:66a98471fc18cad9064123106d810a75f57f0838eee20edc56233fd8484b0cc7", size = 8049868, upload-time = "2025-11-12T15:25:13.058Z" }, @@ -1309,7 +1891,7 @@ wheels = [ [[package]] name = "transformers" version = "5.0.0.dev0" -source = { git = "ssh://git@github.com/Liquid4All/transformers_private.git?branch=lfm2-config#d6cb9404d7d0eb35f2e5dfbe1a248456222b3d4c" } +source = { git = "https://github.com/huggingface/transformers.git?rev=3c25177#3c2517727ce28a30f5044e01663ee204deb1cdbe" } dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, @@ -1329,7 +1911,6 @@ name = "triton" version = "3.5.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802, upload-time = "2025-11-11T17:40:53.209Z" }, { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" }, { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" }, { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" },