diff --git a/README.md b/README.md index cf70064f..ea3d23fc 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ Try OmniVoice without coding: - Launch the local web UI: `omnivoice-demo --ip 0.0.0.0 --port 8001` +- Launch the OpenAI-compatible TTS server: `omnivoice-openai-tts-server --host 0.0.0.0 --port 6655` + +- Open the browser UI: `http://127.0.0.1:6655/ui` - Or try it directly on [HuggingFace Space](https://huggingface.co/spaces/k2-fsa/OmniVoice) @@ -113,13 +116,11 @@ Clone a voice from a short reference audio. Provide `ref_audio` and `ref_text`: ```python from omnivoice import OmniVoice -import torch import torchaudio model = OmniVoice.from_pretrained( "k2-fsa/OmniVoice", device_map="cuda:0", - dtype=torch.float16 ) # Apple Silicon users: use device_map="mps" instead @@ -138,6 +139,7 @@ torchaudio.save("out.wav", audio[0], 24000) > **Tips** > > - Use a 3–10 seconds reference audio clip. Longer audio slows down inference and may degrade cloning quality. +> - On CUDA, OmniVoice now auto-selects an inference-friendly dtype (bf16 on Ampere-or-newer GPUs when available, otherwise fp16) and enables TF32 matmul fast paths. > - For better results with Arabic numerals, normalize them to words first (e.g., "123" → "one hundred twenty-three") with text normalization tools (e.g., [WeTextProcessing](https://github.com/wenet-e2e/WeTextProcessing)). ### Voice Design @@ -216,6 +218,7 @@ Three CLI entry points are provided. The CLI tools support all features availabl | `omnivoice-demo` | Interactive Gradio web demo | [omnivoice/cli/demo.py](omnivoice/cli/demo.py) | | `omnivoice-infer` | Single-item inference | [omnivoice/cli/infer.py](omnivoice/cli/infer.py) | | `omnivoice-infer-batch` | Batch inference across multiple GPUs | [omnivoice/cli/infer_batch.py](omnivoice/cli/infer_batch.py) | +| `omnivoice-openai-tts-server` | OpenAI-compatible FastAPI TTS server with request sanitization and sentence-aware chunking | [omnivoice/openai_tts_server.py](omnivoice/openai_tts_server.py) | ### Demo @@ -269,6 +272,28 @@ Only `id` and `text` are mandatory fields. `ref_audio` and `ref_text` are used i `language_id`, `language_name`, `duration`, and `speed` are optional. `duration` (in seconds) fixes the output length; `speed` controls the speaking rate. If `duration` and `speed` are both provided, `speed` will be ignored. +### OpenAI-Compatible TTS Server + +```bash +omnivoice-openai-tts-server --host 0.0.0.0 --port 6655 +``` + +The server exposes `/v1/audio/speech`, `/v1/audio/voices`, `/v1/audio/models`, and `/health` endpoints. Incoming text is sanitized before synthesis (control-token stripping, whitespace cleanup, optional URL/email/phone normalization), and longer inputs are chunked on sentence punctuation before the final audio is returned as one response. + +The `/ui` page provides a small browser-friendly credits and voice summary view. + +```bash +curl -X POST http://127.0.0.1:6655/v1/audio/speech \ + -H "Content-Type: application/json" \ + -d '{ + "model": "omnivoice", + "voice": "nova", + "input": "Hello world. This sentence-aware request will be sanitized and synthesized.", + "response_format": "mp3" + }' \ + --output speech.mp3 +``` + --- ## Training & Evaluation diff --git a/docs/inference-performance-audit.md b/docs/inference-performance-audit.md new file mode 100644 index 00000000..ee60f270 --- /dev/null +++ b/docs/inference-performance-audit.md @@ -0,0 +1,34 @@ +# OmniVoice inference performance audit + +This note records the current inference-path bottlenecks that were reviewed with an emphasis on **CUDA Ampere GPUs**, plus the fixes landed in this repository. + +## Fixed bottlenecks + +| Area | Bottleneck | Change | +| --- | --- | --- | +| Iterative decoding attention | `omnivoice/models/omnivoice.py` built a dense `(2B, 1, L, L)` boolean attention mask for every inference batch. That is quadratic in sequence length, wastes VRAM, and prevents the inference path from taking advantage of the flex-attention fast path. | Replaced the dense mask with a **BlockMask** built from per-sequence document ids. Padding tokens now keep the old diagonal-only behavior without materializing a dense mask. CUDA inference now defaults to `attn_implementation="flex_attention"` unless the caller overrides it. | +| Logit scoring | The decoder converted the **entire** batched logits tensor to `float32` before slicing out the per-sample target spans. This added avoidable memory traffic and temporary allocations on every decoding step. | Kept batched logits in model dtype and only promote the smaller per-sample slices inside `_predict_tokens_with_scoring()` where the higher-precision log-softmax is actually needed. | +| CUDA runtime selection | The single-item CLI, batch CLI, demo, and server each hard-coded their own device logic, and the GPU-facing entry points always loaded the model in `float16`. That missed a safe Ampere optimization path and made runtime behavior inconsistent across tools. | Centralized runtime helpers in `omnivoice.utils.common`: device detection, auto dtype resolution, and TF32 enablement. Inference now auto-selects **bf16 on Ampere-or-newer CUDA GPUs** when available, otherwise fp16 on CUDA and fp32 elsewhere. | + +## Remaining bottlenecks worth tracking + +These are still present after the changes above and should be treated as follow-up work rather than regressions: + +1. **Reference prompt preprocessing is still repeated outside the server cache path.** The OpenAI-compatible server caches `voice_clone_prompt` objects, but the batch CLI still rebuilds prompts if the same reference audio is used across multiple jobs. +2. **Batch-duration clustering reloads reference audio on CPU.** `omnivoice/cli/infer_batch.py` loads each reference waveform once for duration estimation and again later during generation. A metadata-only duration path would reduce startup cost for large JSONL jobs. +3. **Chunked long-form generation remains sequential within an utterance.** `omnivoice/models/omnivoice.py::_generate_chunked()` batches across items, but chunk dependencies still serialize work within each long sample. +4. **Audio decode/post-process work is still per-item.** The final tokenizer decode, silence trimming, cross-fade, and fade/pad steps remain outside the main generation loop and are not batched. + +## Ampere-specific guidance + +For RTX 30-series and other Ampere GPUs, the best current path in this repo is: + +- use the default CUDA device selection +- let `OmniVoice.from_pretrained(..., device_map="cuda")` choose the dtype automatically +- keep `nj_per_gpu=1` unless you have enough VRAM to hold multiple full model copies per card + +That combination now gives the inference path: + +- BF16 weights/activations when the GPU supports them +- TF32 matmul fast paths for remaining FP32 kernels +- flex-attention block masks instead of dense quadratic masks diff --git a/omnivoice/cli/demo.py b/omnivoice/cli/demo.py index 73dd0a97..4a664080 100644 --- a/omnivoice/cli/demo.py +++ b/omnivoice/cli/demo.py @@ -29,21 +29,12 @@ import gradio as gr import numpy as np -import torch from omnivoice import OmniVoice, OmniVoiceGenerationConfig +from omnivoice.utils.common import get_best_device from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name -def get_best_device(): - """Auto-detect the best available device: CUDA > MPS > CPU.""" - if torch.cuda.is_available(): - return "cuda" - if torch.backends.mps.is_available(): - return "mps" - return "cpu" - - # --------------------------------------------------------------------------- # Language list — all 600+ supported languages # --------------------------------------------------------------------------- @@ -522,7 +513,6 @@ def main(argv=None) -> int: model = OmniVoice.from_pretrained( checkpoint, device_map=device, - dtype=torch.float16, load_asr=not args.no_asr, ) print("Model loaded.") diff --git a/omnivoice/cli/infer.py b/omnivoice/cli/infer.py index f913304e..f25176f1 100644 --- a/omnivoice/cli/infer.py +++ b/omnivoice/cli/infer.py @@ -22,20 +22,10 @@ import argparse import logging -import torch import torchaudio from omnivoice.models.omnivoice import OmniVoice -from omnivoice.utils.common import str2bool - - -def get_best_device(): - """Auto-detect the best available device: CUDA > MPS > CPU.""" - if torch.cuda.is_available(): - return "cuda" - if torch.backends.mps.is_available(): - return "mps" - return "cpu" +from omnivoice.utils.common import get_best_device, str2bool def get_parser() -> argparse.ArgumentParser: @@ -126,9 +116,7 @@ def main(): device = args.device or get_best_device() logging.info(f"Loading model from {args.model} on {device} ...") - model = OmniVoice.from_pretrained( - args.model, device_map=device, dtype=torch.float16 - ) + model = OmniVoice.from_pretrained(args.model, device_map=device) logging.info(f"Generating audio for: {args.text[:80]}...") audios = model.generate( diff --git a/omnivoice/cli/infer_batch.py b/omnivoice/cli/infer_batch.py index f14b06d0..22487ea6 100644 --- a/omnivoice/cli/infer_batch.py +++ b/omnivoice/cli/infer_batch.py @@ -47,20 +47,11 @@ from omnivoice.models.omnivoice import OmniVoice from omnivoice.utils.audio import load_audio -from omnivoice.utils.common import str2bool +from omnivoice.utils.common import get_best_device_and_count, str2bool from omnivoice.utils.data_utils import read_test_list from omnivoice.utils.duration import RuleDurationEstimator -def get_best_device(): - """Auto-detect the best available device: CUDA > MPS > CPU.""" - if torch.cuda.is_available(): - return "cuda", torch.cuda.device_count() - if torch.backends.mps.is_available(): - return "mps", 1 - return "cpu", 1 - - worker_model = None SAMPLING_RATE = 24000 @@ -231,7 +222,6 @@ def process_init(rank_queue, model_checkpoint, warmup=0): worker_model = OmniVoice.from_pretrained( model_checkpoint, device_map=worker_device, - dtype=torch.float16, ) if warmup > 0: @@ -407,11 +397,18 @@ def main(): args = get_parser().parse_args() os.makedirs(args.res_dir, exist_ok=True) - device_type, num_devices = get_best_device() + device_type, num_devices = get_best_device_and_count() if device_type == "cpu": logging.warning( "No GPU found. Falling back to CPU inference. This might be slow." ) + elif device_type.startswith("cuda") and args.nj_per_gpu > 1: + logging.warning( + "nj_per_gpu=%d will load the full model %d time(s) per GPU. " + "For large checkpoints this usually hurts VRAM efficiency and latency.", + args.nj_per_gpu, + args.nj_per_gpu, + ) num_processes = num_devices * args.nj_per_gpu logging.info( diff --git a/omnivoice/models/omnivoice.py b/omnivoice/models/omnivoice.py index a3e8b276..a2e0ecc9 100644 --- a/omnivoice/models/omnivoice.py +++ b/omnivoice/models/omnivoice.py @@ -59,6 +59,11 @@ remove_silence, trim_long_audio, ) +from omnivoice.utils.common import ( + configure_cuda_inference, + resolve_device_string, + resolve_inference_dtype, +) from omnivoice.utils.duration import RuleDurationEstimator from omnivoice.utils.lang_map import LANG_IDS, LANG_NAMES from omnivoice.utils.text import add_punctuation, chunk_text_punctuation @@ -216,6 +221,16 @@ def __init__(self, config: OmniVoiceConfig, llm: Optional[PreTrainedModel] = Non w / sum(config.audio_codebook_weights) for w in config.audio_codebook_weights ] + self.register_buffer( + "normalized_codebook_weight_tensor", + torch.tensor(self.normalized_audio_codebook_weights, dtype=torch.float32), + persistent=False, + ) + self.register_buffer( + "inference_layer_ids", + torch.arange(config.num_audio_codebook, dtype=torch.float32).view(1, -1, 1), + persistent=False, + ) self.post_init() @@ -231,6 +246,29 @@ def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs): train_mode = kwargs.pop("train", False) load_asr = kwargs.pop("load_asr", False) asr_model_name = kwargs.pop("asr_model_name", "openai/whisper-large-v3-turbo") + requested_dtype = kwargs.pop("dtype", None) + requested_torch_dtype = kwargs.pop("torch_dtype", None) + if requested_dtype is not None and requested_torch_dtype is not None: + if requested_dtype != requested_torch_dtype: + raise ValueError( + "dtype and torch_dtype must match when both are provided." + ) + + load_device = kwargs.get("device_map") + resolved_device = resolve_device_string(load_device) + resolved_dtype = ( + requested_dtype if requested_dtype is not None else requested_torch_dtype + ) + + if not train_mode: + kwargs["dtype"] = resolve_inference_dtype( + load_device, "auto" if resolved_dtype is None else resolved_dtype + ) + if resolved_device.startswith("cuda"): + configure_cuda_inference(load_device) + kwargs.setdefault("attn_implementation", "flex_attention") + elif resolved_dtype is not None: + kwargs["dtype"] = resolved_dtype # Suppress noisy INFO logs from transformers/huggingface_hub during loading _prev_disable = logging.root.manager.disable @@ -296,9 +334,7 @@ def load_asr_model(self, model_name: str = "openai/whisper-large-v3-turbo"): from transformers import pipeline as hf_pipeline logger.info("Loading ASR model %s ...", model_name) - asr_dtype = ( - torch.float16 if str(self.device).startswith("cuda") else torch.float32 - ) + asr_dtype = resolve_inference_dtype(self.device) self._asr_pipe = hf_pipeline( "automatic-speech-recognition", model=model_name, @@ -432,10 +468,7 @@ def forward( dim=(0, 2) ) / valid_mask.sum(dim=(0, 2)).clamp(min=1.0) - weights = torch.tensor( - self.normalized_audio_codebook_weights, device=audio_logits.device - ) - loss = (layer_means * weights).sum() + loss = (layer_means * self.normalized_codebook_weight_tensor).sum() return OmniVoiceModelOutput( loss=loss, @@ -537,7 +570,8 @@ def generate( else OmniVoiceGenerationConfig.from_dict(kwargs) ) - self.eval() + if self.training: + self.eval() full_task = self._preprocess_all( text=text, @@ -1169,8 +1203,19 @@ def _generate_iterative( batch_audio_mask = torch.zeros( (2 * B, max_c_len), dtype=torch.bool, device=self.device ) - batch_attention_mask = torch.zeros( - (2 * B, 1, max_c_len, max_c_len), dtype=torch.bool, device=self.device + batch_document_ids = _build_block_mask_document_ids( + c_lens + task.target_lens, + max_seq_len=max_c_len, + device=self.device, + ) + batch_attention_mask = create_block_mask( + partial(_mask_mod_packed, batch_document_ids), + B=2 * B, + H=None, + Q_LEN=max_c_len, + KV_LEN=max_c_len, + _compile=True, + device=self.device, ) for i, inp in enumerate(inputs_list): @@ -1179,15 +1224,10 @@ def _generate_iterative( # Cond (0 ~ B-1) batch_input_ids[i, :, :c_len] = inp["input_ids"] batch_audio_mask[i, :c_len] = inp["audio_mask"] - batch_attention_mask[i, :, :c_len, :c_len] = True # Uncond (B ~ 2B-1) batch_input_ids[B + i, :, :u_len] = inp["input_ids"][..., -u_len:] batch_audio_mask[B + i, :u_len] = inp["audio_mask"][..., -u_len:] - batch_attention_mask[B + i, :, :u_len, :u_len] = True - if max_c_len > u_len: - pad_diag = torch.arange(u_len, max_c_len, device=self.device) - batch_attention_mask[B + i, :, pad_diag, pad_diag] = True tokens = torch.full( (B, self.config.num_audio_codebook, max(task.target_lens)), @@ -1220,16 +1260,12 @@ def _generate_iterative( rem -= int(num) schedules.append(sched) - layer_ids = torch.arange( - self.config.num_audio_codebook, device=self.device - ).view(1, -1, 1) - for step in range(gen_config.num_step): batch_logits = self( input_ids=batch_input_ids, audio_mask=batch_audio_mask, attention_mask=batch_attention_mask, - ).logits.to(torch.float32) + ).logits for i in range(B): k = schedules[i][step] @@ -1247,7 +1283,9 @@ def _generate_iterative( c_logits, u_logits, gen_config ) - scores = scores - (layer_ids * gen_config.layer_penalty_factor) + scores = scores - ( + self.inference_layer_ids * gen_config.layer_penalty_factor + ) if gen_config.position_temperature > 0.0: scores = _gumbel_sample(scores, gen_config.position_temperature) @@ -1270,15 +1308,21 @@ def _generate_iterative( return [tokens[i, :, : task.target_lens[i]] for i in range(B)] def _predict_tokens_with_scoring(self, c_logits, u_logits, gen_config): + log_prob_dtype = ( + torch.float32 + if c_logits.dtype in (torch.float16, torch.bfloat16) + else c_logits.dtype + ) if gen_config.guidance_scale != 0: - c_log_probs = F.log_softmax(c_logits, dim=-1) - u_log_probs = F.log_softmax(u_logits, dim=-1) - log_probs = torch.log_softmax( + c_log_probs = F.log_softmax(c_logits, dim=-1, dtype=log_prob_dtype) + u_log_probs = F.log_softmax(u_logits, dim=-1, dtype=log_prob_dtype) + log_probs = F.log_softmax( c_log_probs + gen_config.guidance_scale * (c_log_probs - u_log_probs), dim=-1, + dtype=log_prob_dtype, ) else: - log_probs = F.log_softmax(c_logits, dim=-1) + log_probs = F.log_softmax(c_logits, dim=-1, dtype=log_prob_dtype) log_probs[..., self.config.audio_mask_id] = -float("inf") @@ -1308,10 +1352,37 @@ def _mask_mod_packed(document_ids, b, h, q_idx, kv_idx): # 1. Sequence Packing Logic: Tokens must belong to the same document. # Note: The doc_id for padding tokens is -1, which will automatically not match # (if handled correctly) or be ignored. - same_doc = document_ids[q_idx] == document_ids[kv_idx] + if document_ids.dim() == 1: + same_doc = document_ids[q_idx] == document_ids[kv_idx] + else: + same_doc = document_ids[b, q_idx] == document_ids[b, kv_idx] return same_doc +def _build_block_mask_document_ids( + lengths: List[int], max_seq_len: int, device: torch.device +) -> torch.Tensor: + """Build per-sequence document ids for non-causal block masks. + + Real tokens share document id 0. Padding tokens receive unique negative ids + so they only attend to themselves, matching the old diagonal-only padding + behavior without materializing a dense quadratic mask. + """ + document_ids = torch.empty( + (len(lengths), max_seq_len), dtype=torch.int32, device=device + ) + for row, seq_len in enumerate(lengths): + document_ids[row, :seq_len] = 0 + if seq_len < max_seq_len: + document_ids[row, seq_len:] = -torch.arange( + 1, + max_seq_len - seq_len + 1, + dtype=torch.int32, + device=device, + ) + return document_ids + + def _resolve_language(language: Optional[str]) -> Union[str, None]: from omnivoice.utils.lang_map import LANG_IDS, LANG_NAME_TO_ID diff --git a/omnivoice/openai_tts_server.py b/omnivoice/openai_tts_server.py new file mode 100644 index 00000000..4c57c349 --- /dev/null +++ b/omnivoice/openai_tts_server.py @@ -0,0 +1,1890 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import asyncio +from html import escape +import logging +import math +import os +import re +import subprocess +import tempfile +import time +import unicodedata +from contextlib import asynccontextmanager +from dataclasses import dataclass +from pathlib import Path +from typing import Literal, Optional +from uuid import uuid4 + +import inflect +import torch +import torchaudio +import uvicorn +from fastapi import FastAPI, HTTPException, Request, Response +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from omnivoice import OmniVoice, OmniVoiceGenerationConfig +from omnivoice.utils.common import get_best_device +from omnivoice.utils.text import add_punctuation, chunk_text_punctuation + + +LOG = logging.getLogger("omnivoice.openai_tts_server") +BACKEND_MODEL_ID = os.getenv("OMNIVOICE_MODEL_ID", "k2-fsa/OmniVoice") +API_MODEL_ID = os.getenv("OMNIVOICE_API_MODEL_ID", "omnivoice") +DEFAULT_HOST = os.getenv("OMNIVOICE_HOST", "0.0.0.0") +DEFAULT_PORT = int(os.getenv("OMNIVOICE_PORT", "6655")) +DEFAULT_VOICE = os.getenv("OMNIVOICE_DEFAULT_VOICE", "alloy") +DEFAULT_AUDIO_CHUNK_DURATION = float( + os.getenv("OMNIVOICE_AUDIO_CHUNK_DURATION", "15.0") +) +DEFAULT_AUDIO_CHUNK_THRESHOLD = float( + os.getenv("OMNIVOICE_AUDIO_CHUNK_THRESHOLD", "30.0") +) +DEFAULT_SENTENCE_CHUNKING_MIN_CHARS = int( + os.getenv("OMNIVOICE_SENTENCE_CHUNKING_MIN_CHARS", "280") +) +MAX_SANITIZED_INPUT_CHARS = int(os.getenv("OMNIVOICE_MAX_INPUT_CHARS", "12000")) +MAX_RAW_INPUT_CHARS = int( + os.getenv("OMNIVOICE_MAX_RAW_INPUT_CHARS", str(MAX_SANITIZED_INPUT_CHARS * 2)) +) +DEBUG_PREVIEW_CHARS = int(os.getenv("OMNIVOICE_DEBUG_PREVIEW_CHARS", "160")) +LOCAL_VOICE_REFERENCE_ROOT = Path( + os.getenv("OMNIVOICE_LOCAL_VOICE_ROOT", "/home/op/Libro-Gregoria-Variacion/audio") +) + +VALID_TLDS = [ + "com", + "org", + "net", + "edu", + "gov", + "mil", + "int", + "biz", + "info", + "name", + "pro", + "coop", + "museum", + "travel", + "jobs", + "mobi", + "tel", + "asia", + "cat", + "xxx", + "aero", + "arpa", + "bg", + "br", + "ca", + "cn", + "de", + "es", + "eu", + "fr", + "in", + "it", + "jp", + "mx", + "nl", + "ru", + "uk", + "us", + "io", + "co", +] +VALID_UNITS = { + "m": "meter", + "cm": "centimeter", + "mm": "millimeter", + "km": "kilometer", + "in": "inch", + "ft": "foot", + "yd": "yard", + "mi": "mile", + "g": "gram", + "kg": "kilogram", + "mg": "milligram", + "s": "second", + "ms": "millisecond", + "min": "minute", + "h": "hour", + "l": "liter", + "ml": "milliliter", + "cl": "centiliter", + "dl": "deciliter", + "kph": "kilometer per hour", + "mph": "mile per hour", + "mi/h": "mile per hour", + "m/s": "meter per second", + "km/h": "kilometer per hour", + "mm/s": "millimeter per second", + "cm/s": "centimeter per second", + "ft/s": "feet per second", + "cm/h": "centimeter per hour", + "degc": "degree celsius", + "degf": "degree fahrenheit", + "°c": "degree celsius", + "°f": "degree fahrenheit", + "pa": "pascal", + "kpa": "kilopascal", + "mpa": "megapascal", + "atm": "atmosphere", + "hz": "hertz", + "khz": "kilohertz", + "mhz": "megahertz", + "ghz": "gigahertz", + "v": "volt", + "kv": "kilovolt", + "mv": "megavolt", + "a": "amp", + "ma": "milliamp", + "ka": "kiloamp", + "w": "watt", + "kw": "kilowatt", + "mw": "megawatt", + "j": "joule", + "kj": "kilojoule", + "mj": "megajoule", + "ω": "ohm", + "kω": "kiloohm", + "mω": "megaohm", + "f": "farad", + "µf": "microfarad", + "uf": "microfarad", + "nf": "nanofarad", + "pf": "picofarad", + "b": "bit", + "kb": "kilobit", + "mb": "megabit", + "gb": "gigabit", + "tb": "terabit", + "pb": "petabit", + "kbps": "kilobit per second", + "mbps": "megabit per second", + "gbps": "gigabit per second", + "tbps": "terabit per second", + "px": "pixel", +} +MONEY_UNITS = {"$": ("dollar", "cent"), "£": ("pound", "pence"), "€": ("euro", "cent")} +INFLECT_ENGINE = inflect.engine() + +SMART_PUNCTUATION_TRANSLATION = str.maketrans( + { + "\u2018": "'", + "\u2019": "'", + "\u201c": '"', + "\u201d": '"', + "\u00ab": '"', + "\u00bb": '"', + "\u2010": "-", + "\u2011": "-", + "\u2012": "-", + "\u2013": "-", + "\u2014": "-", + "\u2212": "-", + "\u2026": "...", + } +) +CJK_PUNCTUATION_TRANSLATION = str.maketrans( + { + "、": ",", + "。": ".", + ",": ",", + ":": ":", + ";": ";", + "!": "!", + "?": "?", + "(": "(", + ")": ")", + } +) +CONTROL_CHAR_PATTERN = re.compile(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]") +MODEL_CONTROL_TOKEN_PATTERN = re.compile(r"<\|[^<>|\r\n]{1,128}\|>") +HTML_TAG_PATTERN = re.compile(r"\r\n]{1,256}>") +URL_PATTERN = re.compile( + r"(https?://|www\.|)+(localhost|[a-zA-Z0-9.-]+(\.(?:" + + "|".join(VALID_TLDS) + + r"))+|[0-9]{1,3}(?:\.[0-9]{1,3}){3})(:[0-9]+)?([/?][^\s]*)?", + re.IGNORECASE, +) +EMAIL_PATTERN = re.compile( + r"\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-z]{2,}\b", re.IGNORECASE +) +PHONE_GROUP_PATTERN = re.compile( + r"(\+?\d{1,2})?([ .-]?)(\(?\d{3}\)?)[\s.-](\d{3})[\s.-](\d{4})" +) +GENERIC_PHONE_PATTERN = re.compile(r"(?": " greater than ", +} + +@dataclass(frozen=True, slots=True) +class VoiceOptionDefinition: + id: str + fallback_name: str + fallback_instruct: Optional[str] + sample_file: Optional[str] = None + sample_ref_text: Optional[str] = None + sample_label: Optional[str] = None + sample_duration_seconds: Optional[float] = None + default_language: Optional[str] = None + + @property + def sample_path(self) -> Optional[Path]: + if self.sample_file is None: + return None + return LOCAL_VOICE_REFERENCE_ROOT / self.sample_file + + def has_local_sample(self) -> bool: + sample_path = self.sample_path + return sample_path is not None and sample_path.is_file() + + def display_name(self) -> str: + if self.has_local_sample() and self.sample_label is not None: + duration = ( + f" ({int(round(self.sample_duration_seconds))}s local reference)" + if self.sample_duration_seconds is not None + else " (local reference)" + ) + return f"{self.id} -> {self.sample_label}{duration}" + return self.fallback_name + + +@dataclass(frozen=True, slots=True) +class ResolvedVoice: + voice_id: str + display_name: str + instruct: Optional[str] + ref_audio_path: Optional[Path] + ref_text: Optional[str] + default_language: Optional[str] + + +VOICE_OPTIONS: list[VoiceOptionDefinition] = [ + VoiceOptionDefinition( + id="alloy", + fallback_name="alloy (male, moderate pitch, american accent)", + fallback_instruct="male, moderate pitch, american accent", + sample_file="omnivoice_refs/britishMan_10s.wav", + sample_ref_text=( + "I'd like to introduce you to the latest changes on happiness from " + "within yourself. You are in charge of your own destiny." + ), + sample_label="British Man", + sample_duration_seconds=10.0, + default_language="en", + ), + VoiceOptionDefinition( + id="echo", + fallback_name="echo (male, low pitch, british accent)", + fallback_instruct="male, low pitch, british accent", + sample_file="omnivoice_refs/CordobesMan_10s.wav", + sample_ref_text=( + "Che, te quiero presentar los ultimos cambios en la Feli que nace " + "de adentro tuyo. Vos sos el que maneja tu propio destino, no hay otro." + ), + sample_label="Cordobes Man", + sample_duration_seconds=10.0, + default_language="es", + ), + VoiceOptionDefinition( + id="fable", + fallback_name="fable (male, high pitch, australian accent)", + fallback_instruct="male, high pitch, australian accent", + sample_file="omnivoice_refs/britishMan_10s.wav", + sample_ref_text=( + "I'd like to introduce you to the latest changes on happiness from " + "within yourself. You are in charge of your own destiny." + ), + sample_label="British Man", + sample_duration_seconds=10.0, + default_language="en", + ), + VoiceOptionDefinition( + id="onyx", + fallback_name="onyx (male, very low pitch, american accent)", + fallback_instruct="male, very low pitch, american accent", + sample_file="omnivoice_refs/CordobesMan_10s.wav", + sample_ref_text=( + "Che, te quiero presentar los ultimos cambios en la Feli que nace " + "de adentro tuyo. Vos sos el que maneja tu propio destino, no hay otro." + ), + sample_label="Cordobes Man", + sample_duration_seconds=10.0, + default_language="es", + ), + VoiceOptionDefinition( + id="nova", + fallback_name="nova (female, moderate pitch, american accent)", + fallback_instruct="female, moderate pitch, american accent", + sample_file="omnivoice_refs/britishWoman_10s.wav", + sample_ref_text=( + "I'd like to introduce you to the latest changes on happiness from " + "within yourself. You are in charge of your own destiny." + ), + sample_label="British Woman", + sample_duration_seconds=10.0, + default_language="en", + ), + VoiceOptionDefinition( + id="shimmer", + fallback_name="shimmer (female, high pitch, british accent)", + fallback_instruct="female, high pitch, british accent", + sample_file="omnivoice_refs/testargentina_8s.wav", + sample_ref_text=( + "Definitivamente el departamento tiene que tener terraza o balcon. " + "Hace mucho tiempo que tenia ganas de usar esta aplicacion para aprender a cocinar." + ), + sample_label="Argentina Voice", + sample_duration_seconds=8.0, + default_language="es", + ), + VoiceOptionDefinition( + id="british_man", + fallback_name="british_man (male, moderate pitch, british accent)", + fallback_instruct="male, moderate pitch, british accent", + sample_file="omnivoice_refs/britishMan_10s.wav", + sample_ref_text=( + "I'd like to introduce you to the latest changes on happiness from " + "within yourself. You are in charge of your own destiny." + ), + sample_label="British Man", + sample_duration_seconds=10.0, + default_language="en", + ), + VoiceOptionDefinition( + id="british_woman", + fallback_name="british_woman (female, moderate pitch, british accent)", + fallback_instruct="female, moderate pitch, british accent", + sample_file="omnivoice_refs/britishWoman_10s.wav", + sample_ref_text=( + "I'd like to introduce you to the latest changes on happiness from " + "within yourself. You are in charge of your own destiny." + ), + sample_label="British Woman", + sample_duration_seconds=10.0, + default_language="en", + ), + VoiceOptionDefinition( + id="cordobes_man", + fallback_name="cordobes_man (male, moderate pitch, argentinian accent)", + fallback_instruct="male, moderate pitch, argentinian accent", + sample_file="omnivoice_refs/CordobesMan_10s.wav", + sample_ref_text=( + "Che, te quiero presentar los ultimos cambios en la Feli que nace " + "de adentro tuyo. Vos sos el que maneja tu propio destino, no hay otro." + ), + sample_label="Cordobes Man", + sample_duration_seconds=10.0, + default_language="es", + ), + VoiceOptionDefinition( + id="argentina_voice", + fallback_name="argentina_voice (female, warm pitch, argentinian accent)", + fallback_instruct="female, warm pitch, argentinian accent", + sample_file="omnivoice_refs/testargentina_8s.wav", + sample_ref_text=( + "Definitivamente el departamento tiene que tener terraza o balcon. " + "Hace mucho tiempo que tenia ganas de usar esta aplicacion para aprender a cocinar." + ), + sample_label="Argentina Voice", + sample_duration_seconds=8.0, + default_language="es", + ), + VoiceOptionDefinition( + id="auto", + fallback_name="auto (voice design)", + fallback_instruct=None, + ), +] +VOICE_LOOKUP = {item.id: item for item in VOICE_OPTIONS} +SUPPORTED_MODEL_OPTIONS = [ + {"id": API_MODEL_ID, "name": "OmniVoice"}, + {"id": "tts-1", "name": "OmniVoice (OpenAI alias)"}, + {"id": "tts-1-hd", "name": "OmniVoice HD (OpenAI alias)"}, + {"id": "gpt-4o-mini-tts", "name": "OmniVoice mini TTS (OpenAI alias)"}, +] +SUPPORTED_MODEL_ALIASES = { + API_MODEL_ID, + BACKEND_MODEL_ID, + "tts-1", + "tts-1-hd", + "gpt-4o-mini-tts", +} +SUPPORTED_RESPONSE_FORMATS = { + "mp3": ("audio/mpeg", "mp3"), + "wav": ("audio/wav", "wav"), + "flac": ("audio/flac", "flac"), + "ogg": ("audio/ogg", "ogg"), + "opus": ("audio/opus", "opus"), +} + + +def _build_frontend_page() -> str: + voice_cards = [] + local_voice_count = 0 + + for option in VOICE_OPTIONS: + has_local_sample = option.has_local_sample() + if has_local_sample: + local_voice_count += 1 + + source_label = "Local reference clip" if has_local_sample else "Voice design prompt" + duration_label = ( + f"{int(round(option.sample_duration_seconds))}s clip" + if option.sample_duration_seconds is not None + else "No reference clip" + ) + language_label = option.default_language or "auto" + detail_text = option.sample_ref_text or option.fallback_instruct or "Automatic voice selection" + + voice_cards.append( + f""" +
+
+ {escape(option.id)} + {escape(source_label)} +
+

{escape(option.display_name())}

+

Language: {escape(language_label)} · {escape(duration_label)}

+

{escape(detail_text)}

+
+ """ + ) + + return f""" + + + + + OmniVoice | OpenAI-compatible TTS + + + +
+
+

OpenAI-compatible TTS

+

OmniVoice

+

+ FastAPI text-to-speech on 0.0.0.0:6655 with request sanitization, + sentence-aware chunking, and local voice presets that work cleanly with Open WebUI. +

+ +
+
Local voice presets{len(VOICE_OPTIONS)}
+
Local reference clips{local_voice_count}
+
API models{len(SUPPORTED_MODEL_OPTIONS)}
+
Sentence chunkingEnabled
+
+
+ +
+

Voice presets

+

+ Each preset maps to either a compact local reference clip or a direct voice-design + prompt, so Open WebUI can pick a stable voice without guessing. +

+
+ {"".join(voice_cards)} +
+
+ +
+

Credits

+

+ Thanks to the original OmniVoice creators and contributors for the model, research, + and open-source implementation this server is built on. +

+ +
+
+ +""" + + +def _render_frontend_page() -> HTMLResponse: + return HTMLResponse(_build_frontend_page()) + + +def _configure_logging() -> None: + if getattr(_configure_logging, "_configured", False): + return + logging.basicConfig( + format="%(asctime)s %(levelname)s [%(name)s:%(lineno)d] %(message)s", + level=logging.INFO, + force=False, + ) + _configure_logging._configured = True + + +DEFAULT_DEVICE = get_best_device() + + +class TextSanitizationOptions(BaseModel): + model_config = ConfigDict(extra="ignore") + + normalize: bool = True + unit_normalization: bool = False + strip_html: bool = True + strip_model_control_tokens: bool = True + quote_normalization: bool = True + url_normalization: bool = True + email_normalization: bool = True + optional_pluralization_normalization: bool = True + phone_normalization: bool = True + number_normalization: bool = True + money_normalization: bool = True + time_normalization: bool = True + replace_remaining_symbols: bool = True + + +class SpeechRequest(BaseModel): + model_config = ConfigDict(extra="ignore") + + input: Optional[str] = Field(default=None, max_length=MAX_RAW_INPUT_CHARS) + text: Optional[str] = Field(default=None, max_length=MAX_RAW_INPUT_CHARS) + model: Optional[str] = None + voice: Optional[str] = Field(default=None, max_length=512) + response_format: Literal["mp3", "wav", "flac", "ogg", "opus"] = "mp3" + speed: float = Field(default=1.0, gt=0.0, le=4.0) + language: Optional[str] = Field(default=None, max_length=64) + ref_text: Optional[str] = Field(default=None, max_length=MAX_RAW_INPUT_CHARS) + instruct: Optional[str] = Field(default=None, max_length=1024) + duration: Optional[float] = Field(default=None, gt=0.0, le=3600.0) + num_step: Optional[int] = Field(default=None, ge=1, le=128) + guidance_scale: Optional[float] = Field(default=None, ge=0.0, le=20.0) + t_shift: Optional[float] = Field(default=None, ge=0.0, le=1.0) + denoise: Optional[bool] = None + postprocess_output: Optional[bool] = None + layer_penalty_factor: Optional[float] = Field(default=None, ge=0.0, le=20.0) + position_temperature: Optional[float] = Field(default=None, ge=0.0, le=20.0) + class_temperature: Optional[float] = Field(default=None, ge=0.0, le=20.0) + normalization_options: TextSanitizationOptions = Field( + default_factory=TextSanitizationOptions + ) + sentence_chunking: bool = True + sentence_chunking_min_chars: int = Field( + default=DEFAULT_SENTENCE_CHUNKING_MIN_CHARS, + ge=64, + le=MAX_SANITIZED_INPUT_CHARS, + ) + audio_chunk_duration: Optional[float] = Field(default=None, gt=0.5, le=60.0) + audio_chunk_threshold: Optional[float] = Field(default=None, ge=0.0, le=600.0) + + @model_validator(mode="after") + def _validate_text_fields(self) -> SpeechRequest: + values = [value for value in (self.input, self.text) if value is not None] + if not values: + raise ValueError("One of 'input' or 'text' must be provided") + if len(values) == 2 and values[0].strip() != values[1].strip(): + raise ValueError( + "When both 'input' and 'text' are provided they must contain the same text" + ) + return self + + def raw_text(self) -> str: + return self.input if self.input is not None else self.text or "" + + +@dataclass(slots=True) +class PreparedSpeechRequest: + text: str + instruct: Optional[str] + ref_text: Optional[str] + language: Optional[str] + response_format: str + voice_id: str + voice_display_name: str + voice_ref_audio_path: Optional[Path] + voice_ref_text: Optional[str] + chunk_plan: list[str] + force_sentence_chunking: bool + generation_config: OmniVoiceGenerationConfig + + +class OmniVoiceService: + def __init__(self, model_id: str, device: str, idle_timeout: float = 300.0) -> None: + self.model_id = model_id + self.device = device + self.model: OmniVoice | None = None + self.load_error: str | None = None + self.load_lock: asyncio.Lock | None = None + self._idle_timeout = idle_timeout + self._idle_task: asyncio.Task | None = None + self._last_used: float = 0.0 + self._voice_prompt_cache: dict[str, object] = {} + + def set_lock(self, lock: asyncio.Lock) -> None: + self.load_lock = lock + + def _touch(self) -> None: + self._last_used = time.monotonic() + self._schedule_idle_offload() + + def _schedule_idle_offload(self) -> None: + if self._idle_task is not None: + self._idle_task.cancel() + if self._idle_timeout > 0 and self.model is not None: + self._idle_task = asyncio.get_event_loop().create_task( + self._idle_offload_loop() + ) + + async def _idle_offload_loop(self) -> None: + try: + while True: + elapsed = time.monotonic() - self._last_used + remaining = self._idle_timeout - elapsed + if remaining <= 0: + break + await asyncio.sleep(remaining) + except asyncio.CancelledError: + return + + if self.model is not None: + LOG.info( + "Idle timeout (%.0fs) reached, offloading model from %s", + self._idle_timeout, + self.device, + ) + try: + if self.device.startswith("cuda"): + await asyncio.to_thread(self._offload_model_sync) + else: + self.model = None + LOG.info("Model offloaded successfully") + except Exception: + LOG.exception("Failed to offload model") + + def _offload_model_sync(self) -> None: + if self.model is None: + return + import gc + + self.model.cpu() + del self.model + self.model = None + self._voice_prompt_cache.clear() + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + + def _load_model_sync(self) -> OmniVoice: + LOG.info("Loading OmniVoice model %s on %s", self.model_id, self.device) + if self.device.startswith("cuda"): + from transformers import modeling_utils + + original_allocator_warmup = modeling_utils.caching_allocator_warmup + + # The default Transformers warmup can briefly allocate several extra + # GiB on cold load. Skipping it keeps OmniVoice lazy-loading viable + # on the RTX 3060 when other workloads already share that card. + modeling_utils.caching_allocator_warmup = lambda *args, **kwargs: None + try: + model = OmniVoice.from_pretrained( + self.model_id, + device_map=self.device, + ) + finally: + modeling_utils.caching_allocator_warmup = original_allocator_warmup + else: + model = OmniVoice.from_pretrained( + self.model_id, + device_map=self.device, + ) + model.eval() + LOG.info( + "Model loaded on %s with sampling rate %s", + model.device, + getattr(model, "sampling_rate", "unknown"), + ) + return model + + async def get_model(self) -> OmniVoice: + if self.model is not None: + self._touch() + return self.model + if self.load_lock is None: + raise RuntimeError("Load lock is not initialized") + + async with self.load_lock: + if self.model is not None: + self._touch() + return self.model + + try: + self.load_error = None + self.model = await asyncio.to_thread(self._load_model_sync) + self._touch() + except Exception as exc: + self.load_error = str(exc) + LOG.exception("Failed to load OmniVoice") + raise + + return self.model + + def health(self) -> dict[str, object]: + return { + "model_id": self.model_id, + "api_model_id": API_MODEL_ID, + "device": self.device, + "model_loaded": self.model is not None, + "load_error": self.load_error, + "default_voice": DEFAULT_VOICE, + "idle_timeout": self._idle_timeout, + } + + def get_or_create_voice_clone_prompt( + self, + model: OmniVoice, + *, + cache_key: str, + ref_audio_path: Path, + ref_text: Optional[str] = None, + ): + cached = self._voice_prompt_cache.get(cache_key) + if cached is not None: + return cached + + prompt = model.create_voice_clone_prompt( + ref_audio=str(ref_audio_path), + ref_text=ref_text, + ) + self._voice_prompt_cache[cache_key] = prompt + return prompt + + +def _protect_bracket_tags(text: str) -> tuple[str, dict[str, str]]: + protected: dict[str, str] = {} + + def _replace(match: re.Match[str]) -> str: + key = f"OMNIVOICETAG{uuid4().hex.upper()}X{len(protected)}TAG" + protected[key] = match.group(0) + return key + + return PROTECTED_TAG_PATTERN.sub(_replace, text), protected + + +def _restore_bracket_tags(text: str, protected: dict[str, str]) -> str: + for key, value in protected.items(): + text = text.replace(key, value) + return text + + +def _normalize_email(match: re.Match[str]) -> str: + user, domain = match.group(0).split("@", 1) + user = ( + user.replace(".", " dot ").replace("_", " underscore ").replace("-", " dash ") + ) + domain = domain.replace(".", " dot ").replace("-", " dash ") + return f"{user} at {domain}" + + +def _normalize_url(match: re.Match[str]) -> str: + original = match.group(0) + url = original.rstrip(TRAILING_URL_PUNCTUATION) + trailing = original[len(url) :] + spoken = url + spoken = re.sub( + r"^https?://", + lambda item: "https " if item.group(0).lower().startswith("https") else "http ", + spoken, + flags=re.IGNORECASE, + ) + spoken = re.sub(r"^www\.", "www ", spoken, flags=re.IGNORECASE) + spoken = spoken.replace(".", " dot ") + spoken = spoken.replace("/", " slash ") + spoken = spoken.replace("?", " question mark ") + spoken = spoken.replace("=", " equals ") + spoken = spoken.replace("&", " and ") + spoken = spoken.replace("-", " dash ") + spoken = spoken.replace("_", " underscore ") + spoken = spoken.replace(":", " colon ") + spoken = WHITESPACE_PATTERN.sub(" ", spoken).strip() + return f"{spoken}{trailing}" + + +def _normalize_phone(match: re.Match[str]) -> str: + value = match.group(0) + digits = [char for char in value if char.isdigit()] + if not digits: + return value + prefix = "plus " if value.strip().startswith("+") else "" + return prefix + " ".join(digits) + + +def _should_apply_english_expansions(language: Optional[str]) -> bool: + if language is None: + return True + language_key = language.lower().replace("_", "-").split("-", 1)[0] + return language_key in {"en", "eng"} + + +def _conditional_int(number: float, threshold: float = 0.00001): + if abs(round(number) - number) < threshold: + return int(round(number)) + return number + + +def _translate_multiplier(multiplier: str) -> str: + mapping = { + "k": "thousand", + "m": "million", + "b": "billion", + "t": "trillion", + } + key = multiplier.strip().lower() + return mapping.get(key, multiplier.strip()) + + +def _split_four_digit_year(number: float) -> str: + number_str = str(_conditional_int(number)) + return ( + f"{INFLECT_ENGINE.number_to_words(number_str[:2])} " + f"{INFLECT_ENGINE.number_to_words(number_str[2:])}" + ) + + +def _normalize_unit(match: re.Match[str]) -> str: + unit_string = match.group(6).strip() + unit_name = VALID_UNITS.get(unit_string.lower()) + if unit_name is None: + return match.group(0) + + parts = unit_name.split(" ") + number = match.group(1).strip().replace(",", "") + if parts[0].endswith("bit") and unit_string[-1:] == "B": + parts[0] = parts[0][:-3] + "byte" + parts[0] = INFLECT_ENGINE.no(parts[0], number) + return " ".join(parts) + + +def _normalize_grouped_phone(match: re.Match[str]) -> str: + country_code, _, area_code, telephone_prefix, line_number = match.groups() + parts: list[str] = [] + if country_code: + parts.append( + INFLECT_ENGINE.number_to_words( + country_code.replace("+", ""), + group=1, + comma="", + ) + ) + parts.append( + INFLECT_ENGINE.number_to_words( + area_code.replace("(", "").replace(")", ""), + group=1, + comma="", + ) + ) + parts.append( + INFLECT_ENGINE.number_to_words(telephone_prefix, group=1, comma="") + ) + parts.append(INFLECT_ENGINE.number_to_words(line_number, group=1, comma="")) + return ", ".join(filter(None, parts)) + + +def _normalize_time(match: re.Match[str]) -> str: + time_value = match.group(1) + meridiem = (match.group(2) or "").strip().lower() + time_parts = [part.strip() for part in time_value.split(":")] + + result = [INFLECT_ENGINE.number_to_words(int(time_parts[0]))] + minutes = int(time_parts[1]) + if minutes == 0: + result.append("o'clock") + elif minutes < 10: + result.append(f"oh {INFLECT_ENGINE.number_to_words(minutes)}") + else: + result.append(INFLECT_ENGINE.number_to_words(minutes)) + + if len(time_parts) > 2: + seconds = int(time_parts[2]) + result.append( + f"and {INFLECT_ENGINE.number_to_words(seconds)} " + f"{INFLECT_ENGINE.plural('second', seconds)}" + ) + + if meridiem: + result.append(meridiem) + return " ".join(result) + + +def _normalize_money(match: re.Match[str]) -> str: + bill, coin = MONEY_UNITS[match.group(2)] + amount_text = match.group(3) + try: + amount = float(amount_text) + except ValueError: + return match.group(0) + + if match.group(1) == "-": + amount *= -1 + + multiplier = _translate_multiplier(match.group(4)) + multiplier_suffix = f" {multiplier}" if multiplier else "" + abs_amount = abs(amount) + + if abs_amount % 1 == 0 or multiplier: + spoken_amount = INFLECT_ENGINE.number_to_words(_conditional_int(abs_amount)) + spoken_bill = INFLECT_ENGINE.plural( + bill, + count=max(int(abs_amount), 1), + ) + prefix = "minus " if amount < 0 else "" + return f"{prefix}{spoken_amount}{multiplier_suffix} {spoken_bill}" + + whole_amount = int(math.floor(abs_amount)) + cents = int(amount_text.split(".")[-1].ljust(2, "0")) + spoken_bill = INFLECT_ENGINE.plural(bill, count=max(whole_amount, 1)) + prefix = "minus " if amount < 0 else "" + return ( + f"{prefix}{INFLECT_ENGINE.number_to_words(whole_amount)} {spoken_bill} and " + f"{INFLECT_ENGINE.number_to_words(cents)} " + f"{INFLECT_ENGINE.plural(coin, count=cents)}" + ) + + +def _normalize_number(match: re.Match[str]) -> str: + try: + number = float(match.group(2)) + except ValueError: + return match.group(0) + + if match.group(1) == "-": + number *= -1 + + multiplier = _translate_multiplier(match.group(3)) + if not multiplier: + abs_number = abs(number) + if ( + abs_number % 1 == 0 + and len(str(int(abs_number))) == 4 + and abs_number > 1500 + and int(abs_number) % 1000 > 9 + ): + prefix = "minus " if number < 0 else "" + return f"{prefix}{_split_four_digit_year(abs_number)}" + + spoken_number = INFLECT_ENGINE.number_to_words(_conditional_int(abs(number))) + prefix = "minus " if number < 0 else "" + multiplier_suffix = f" {multiplier}" if multiplier else "" + return f"{prefix}{spoken_number}{multiplier_suffix}" + + +def _normalize_titles_and_abbreviations(text: str) -> str: + text = re.sub(r"\bD[Rr]\.(?= [A-Z])", "Doctor", text) + text = re.sub(r"\b(?:Mr\.|MR\.(?= [A-Z]))", "Mister", text) + text = re.sub(r"\b(?:Ms\.|MS\.(?= [A-Z]))", "Miss", text) + text = re.sub(r"\b(?:Mrs\.|MRS\.(?= [A-Z]))", "Mrs", text) + text = re.sub(r"\betc\.(?! [A-Z])", "etc", text) + text = re.sub(r"(?i)\b(y)eah?\b", r"\1e'a", text) + return text + + +def _normalize_english_like_text( + text: str, options: TextSanitizationOptions +) -> str: + normalized = text + if options.unit_normalization: + normalized = UNIT_PATTERN.sub(_normalize_unit, normalized) + if options.optional_pluralization_normalization: + normalized = OPTIONAL_PLURALIZATION_PATTERN.sub("s", normalized) + normalized = re.sub(r"(?<=\d)s\b", " s", normalized, flags=re.IGNORECASE) + if options.phone_normalization: + normalized = PHONE_GROUP_PATTERN.sub(_normalize_grouped_phone, normalized) + if options.time_normalization: + normalized = TIME_PATTERN.sub(_normalize_time, normalized) + + normalized = _normalize_titles_and_abbreviations(normalized) + normalized = THOUSANDS_SEPARATOR_PATTERN.sub("", normalized) + + if options.money_normalization: + normalized = MONEY_PATTERN.sub(_normalize_money, normalized) + if options.number_normalization: + normalized = NUMBER_PATTERN.sub(_normalize_number, normalized) + + return normalized + + +def _basic_cleanup( + text: str, + *, + strip_html: bool, + strip_model_control_tokens: bool, + normalize_quotes: bool, +) -> str: + if normalize_quotes: + text = text.translate(SMART_PUNCTUATION_TRANSLATION) + text = unicodedata.normalize("NFKC", text) + text = text.translate(CJK_PUNCTUATION_TRANSLATION) + if strip_model_control_tokens: + text = MODEL_CONTROL_TOKEN_PATTERN.sub(" ", text) + if strip_html: + text = HTML_TAG_PATTERN.sub(" ", text) + text = CONTROL_CHAR_PATTERN.sub(" ", text) + text = text.replace("\r", " ").replace("\n", " ").replace("\t", " ") + text = WHITESPACE_PATTERN.sub(" ", text) + return text.strip() + + +def sanitize_speech_text( + text: str, + *, + language: Optional[str], + options: TextSanitizationOptions, +) -> str: + if not text: + return "" + if not options.normalize: + return add_punctuation( + _basic_cleanup( + text, + strip_html=options.strip_html, + strip_model_control_tokens=options.strip_model_control_tokens, + normalize_quotes=options.quote_normalization, + ) + ) + + protected_text, protected = _protect_bracket_tags(text) + sanitized = _basic_cleanup( + protected_text, + strip_html=options.strip_html, + strip_model_control_tokens=options.strip_model_control_tokens, + normalize_quotes=options.quote_normalization, + ) + + if options.email_normalization: + sanitized = EMAIL_PATTERN.sub(_normalize_email, sanitized) + if options.url_normalization: + sanitized = URL_PATTERN.sub(_normalize_url, sanitized) + if _should_apply_english_expansions(language): + sanitized = _normalize_english_like_text(sanitized, options) + elif options.optional_pluralization_normalization: + sanitized = OPTIONAL_PLURALIZATION_PATTERN.sub("s", sanitized) + if options.phone_normalization: + sanitized = GENERIC_PHONE_PATTERN.sub(_normalize_phone, sanitized) + if options.replace_remaining_symbols: + for symbol, replacement in SYMBOL_REPLACEMENTS.items(): + sanitized = sanitized.replace(symbol, replacement) + + sanitized = RANGE_DASH_PATTERN.sub(" to ", sanitized) + sanitized = re.sub( + r"(?:[A-Za-z]\.){2,} [a-z]", + lambda match: match.group(0).replace(".", "-"), + sanitized, + ) + sanitized = re.sub(r"(?i)(?<=[A-Z])\.(?=[A-Z])", "-", sanitized) + sanitized = WHITESPACE_PATTERN.sub(" ", sanitized).strip() + sanitized = _restore_bracket_tags(sanitized, protected) + if language is not None: + LOG.debug("Sanitized text for language=%s -> %s", language, sanitized[:120]) + return add_punctuation(sanitized) + + +def sanitize_prompt_text(text: Optional[str]) -> Optional[str]: + if text is None: + return None + cleaned = _basic_cleanup( + text, + strip_html=True, + strip_model_control_tokens=True, + normalize_quotes=True, + ) + return cleaned or None + + +def _plan_sentence_chunks(text: str, min_chars: int) -> list[str]: + if len(text) <= min_chars: + return [text] + planned = chunk_text_punctuation( + text=text, + chunk_len=min_chars, + min_chunk_len=max(24, min_chars // 4), + ) + return planned or [text] + + +def _supported_models() -> list[dict[str, str]]: + return [dict(item) for item in SUPPORTED_MODEL_OPTIONS] + + +def _supported_voices() -> list[dict[str, str]]: + return [{"id": item.id, "name": item.display_name()} for item in VOICE_OPTIONS] + + +def _truncate_preview(text: Optional[str], limit: int = DEBUG_PREVIEW_CHARS) -> str: + if not text: + return "" + preview = WHITESPACE_PATTERN.sub(" ", text).strip() + if len(preview) <= limit: + return preview + return f"{preview[:limit].rstrip()}..." + + +def _normalization_summary(options: TextSanitizationOptions) -> str: + enabled = [key for key, value in options.model_dump().items() if value] + return ",".join(enabled) if enabled else "none" + + +def _guess_request_source(request: Request) -> str: + user_agent = (request.headers.get("user-agent") or "").strip() + user_agent_lower = user_agent.lower() + if "python-requests" in user_agent_lower: + return "python-requests/open-webui-like" + if "mozilla" in user_agent_lower: + return "browser" + if user_agent: + return user_agent[:80] + return "unknown" + + +def _resolve_model(requested: Optional[str]) -> str: + if not requested: + return API_MODEL_ID + if requested in SUPPORTED_MODEL_ALIASES or requested == service.model_id: + return API_MODEL_ID + raise HTTPException( + status_code=400, + detail=( + f"Unsupported model '{requested}'. Supported aliases: " + f"{', '.join(sorted(SUPPORTED_MODEL_ALIASES))}" + ), + ) + + +def _resolve_voice(requested: Optional[str]) -> ResolvedVoice: + voice = (requested or DEFAULT_VOICE).strip() + if not voice: + raise HTTPException(status_code=400, detail="voice must not be empty") + + voice_key = voice.lower() + preset = VOICE_LOOKUP.get(voice_key) + if preset is not None: + use_local_sample = preset.has_local_sample() + return ResolvedVoice( + voice_id=preset.id, + display_name=preset.display_name(), + instruct=None if use_local_sample else preset.fallback_instruct, + ref_audio_path=preset.sample_path if use_local_sample else None, + ref_text=preset.sample_ref_text if use_local_sample else None, + default_language=preset.default_language, + ) + + return ResolvedVoice( + voice_id=voice, + display_name=voice, + instruct=voice, + ref_audio_path=None, + ref_text=None, + default_language=None, + ) + + +def _prepare_request(payload: SpeechRequest) -> PreparedSpeechRequest: + _resolve_model(payload.model) + response_format = payload.response_format + resolved_voice = _resolve_voice(payload.voice) + effective_language = payload.language or resolved_voice.default_language + + text = sanitize_speech_text( + payload.raw_text(), + language=effective_language, + options=payload.normalization_options, + ) + if not text: + raise HTTPException( + status_code=400, detail="Input text is empty after sanitization" + ) + if len(text) > MAX_SANITIZED_INPUT_CHARS: + raise HTTPException( + status_code=400, + detail=( + f"Input text is too long after sanitization ({len(text)} chars). " + f"Maximum allowed is {MAX_SANITIZED_INPUT_CHARS}." + ), + ) + + instruct = sanitize_prompt_text(payload.instruct) + if instruct is None and resolved_voice.ref_audio_path is None: + instruct = sanitize_prompt_text(resolved_voice.instruct) + + resolved_ref_text = ( + sanitize_prompt_text(payload.ref_text) + if payload.ref_text is not None + else sanitize_prompt_text(resolved_voice.ref_text) + ) + ref_text = resolved_ref_text if resolved_voice.ref_audio_path is None else None + chunk_plan = _plan_sentence_chunks(text, payload.sentence_chunking_min_chars) + force_sentence_chunking = payload.sentence_chunking and len(chunk_plan) > 1 + + generation_config_kwargs: dict[str, object] = {} + for field_name in ( + "num_step", + "guidance_scale", + "t_shift", + "denoise", + "postprocess_output", + "layer_penalty_factor", + "position_temperature", + "class_temperature", + ): + value = getattr(payload, field_name) + if value is not None: + generation_config_kwargs[field_name] = value + + generation_config_kwargs["audio_chunk_duration"] = ( + payload.audio_chunk_duration + if payload.audio_chunk_duration is not None + else DEFAULT_AUDIO_CHUNK_DURATION + ) + generation_config_kwargs["audio_chunk_threshold"] = ( + 0.0 + if force_sentence_chunking + else ( + payload.audio_chunk_threshold + if payload.audio_chunk_threshold is not None + else DEFAULT_AUDIO_CHUNK_THRESHOLD + ) + ) + + return PreparedSpeechRequest( + text=text, + instruct=instruct, + ref_text=ref_text, + language=effective_language, + response_format=response_format, + voice_id=resolved_voice.voice_id, + voice_display_name=resolved_voice.display_name, + voice_ref_audio_path=resolved_voice.ref_audio_path, + voice_ref_text=resolved_ref_text if resolved_voice.ref_audio_path is not None else None, + chunk_plan=chunk_plan, + force_sentence_chunking=force_sentence_chunking, + generation_config=OmniVoiceGenerationConfig.from_dict(generation_config_kwargs), + ) + + +def _waveform_to_bytes( + waveform: torch.Tensor, + sample_rate: int, + response_format: str, +) -> tuple[bytes, str]: + media_type, suffix = SUPPORTED_RESPONSE_FORMATS[response_format] + + if waveform.ndim == 1: + waveform = waveform.unsqueeze(0) + + waveform = waveform.detach().cpu().float().clamp(-1.0, 1.0).contiguous() + + with tempfile.TemporaryDirectory(prefix="omnivoice-tts-") as tmp_dir: + tmp_path = Path(tmp_dir) + wav_path = tmp_path / "output.wav" + torchaudio.save(str(wav_path), waveform, sample_rate, format="wav") + + if response_format == "wav": + return wav_path.read_bytes(), media_type + + out_path = tmp_path / f"output.{suffix}" + ffmpeg_cmd = [ + "ffmpeg", + "-y", + "-loglevel", + "error", + "-i", + str(wav_path), + ] + + if response_format == "mp3": + ffmpeg_cmd += ["-codec:a", "libmp3lame", "-b:a", "192k"] + elif response_format == "flac": + ffmpeg_cmd += ["-codec:a", "flac"] + elif response_format in {"ogg", "opus"}: + ffmpeg_cmd += ["-codec:a", "libopus"] + else: + raise HTTPException( + status_code=400, + detail=f"Unsupported response_format '{response_format}'", + ) + + ffmpeg_cmd.append(str(out_path)) + try: + subprocess.run( + ffmpeg_cmd, + check=True, + capture_output=True, + text=True, + ) + except FileNotFoundError as exc: + raise HTTPException( + status_code=500, + detail="ffmpeg is required to encode the requested audio format", + ) from exc + except subprocess.CalledProcessError as exc: + stderr = (exc.stderr or "").strip() + raise HTTPException( + status_code=500, + detail=f"Audio encoding failed: {stderr or exc}", + ) from exc + return out_path.read_bytes(), media_type + + +async def _synthesize_prepared( + prepared: PreparedSpeechRequest, + payload: SpeechRequest, + *, + request_id: Optional[str] = None, +) -> tuple[PreparedSpeechRequest, bytes, str]: + model = await service.get_model() + + generation_kwargs: dict[str, object] = { + "text": prepared.text, + "language": prepared.language, + "speed": payload.speed, + "generation_config": prepared.generation_config, + } + if prepared.ref_text is not None: + generation_kwargs["ref_text"] = prepared.ref_text + if prepared.instruct is not None: + generation_kwargs["instruct"] = prepared.instruct + if payload.duration is not None: + generation_kwargs["duration"] = payload.duration + + LOG.info( + "Synthesizing speech (voice=%s, response_format=%s, text_chars=%d, text_chunks=%d, forced_sentence_chunking=%s, voice_source=%s)", + prepared.voice_id, + prepared.response_format, + len(prepared.text), + len(prepared.chunk_plan), + prepared.force_sentence_chunking, + "local-reference" if prepared.voice_ref_audio_path is not None else "style-prompt", + ) + LOG.debug("Sanitized input preview: %s", prepared.text[:200]) + + def _run_generation() -> tuple[bytes, str]: + generation_args = dict(generation_kwargs) + if prepared.voice_ref_audio_path is not None: + generation_args["voice_clone_prompt"] = service.get_or_create_voice_clone_prompt( + model, + cache_key=( + f"{prepared.voice_ref_audio_path.resolve()}::{prepared.voice_ref_text!r}" + ), + ref_audio_path=prepared.voice_ref_audio_path, + ref_text=prepared.voice_ref_text, + ) + with torch.inference_mode(): + audios = model.generate(**generation_args) + if not audios: + raise RuntimeError("OmniVoice returned no audio") + waveform = audios[0] + return _waveform_to_bytes( + waveform, + int(model.sampling_rate), + prepared.response_format, + ) + + try: + audio_bytes, media_type = await asyncio.to_thread(_run_generation) + except HTTPException: + raise + except Exception as exc: + LOG.exception( + "Speech synthesis failed (request_id=%s, voice=%s, voice_source=%s, language=%s)", + request_id or "n/a", + prepared.voice_id, + "local-reference" if prepared.voice_ref_audio_path is not None else "style-prompt", + prepared.language, + ) + raise HTTPException(status_code=500, detail="Speech synthesis failed") from exc + + return prepared, audio_bytes, media_type + + +@asynccontextmanager +async def lifespan(_: FastAPI): + _configure_logging() + service.set_lock(asyncio.Lock()) + LOG.info( + "Starting OmniVoice TTS server (api_model=%s, backend_model=%s, device=%s, idle_timeout=%.0fs)", + API_MODEL_ID, + service.model_id, + service.device, + service._idle_timeout, + ) + yield + if service._idle_task is not None: + service._idle_task.cancel() + if service.model is not None: + LOG.info("Shutting down, offloading model...") + try: + if service.device.startswith("cuda"): + await asyncio.to_thread(service._offload_model_sync) + else: + service.model = None + except Exception: + LOG.exception("Error offloading model during shutdown") + + +service = OmniVoiceService(BACKEND_MODEL_ID, DEFAULT_DEVICE) +app = FastAPI(title="OmniVoice OpenAI-Compatible TTS", lifespan=lifespan) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.middleware("http") +async def _request_logging_middleware(request: Request, call_next): + start = time.perf_counter() + request_id = uuid4().hex[:12] + request.state.request_id = request_id + request.state.request_source = _guess_request_source(request) + try: + response = await call_next(request) + except Exception: + duration_ms = (time.perf_counter() - start) * 1000 + LOG.exception( + "Request failed (request_id=%s, source=%s, client=%s, %s %s, %.1fms)", + request_id, + request.state.request_source, + getattr(request.client, "host", "unknown"), + request.method, + request.url.path, + duration_ms, + ) + raise + + duration_ms = (time.perf_counter() - start) * 1000 + response.headers["X-OmniVoice-Request-Id"] = request_id + if request.url.path not in {"/", "/health"}: + LOG.info( + "%s %s -> %s in %.1fms (request_id=%s, source=%s, client=%s)", + request.method, + request.url.path, + response.status_code, + duration_ms, + request_id, + request.state.request_source, + getattr(request.client, "host", "unknown"), + ) + return response + + +@app.get("/") +@app.get("/health") +@app.get("/v1/health") +async def health() -> dict[str, object]: + return { + "status": "ok", + "supported_response_formats": sorted(SUPPORTED_RESPONSE_FORMATS), + **service.health(), + } + + +@app.get("/ui", response_class=HTMLResponse) +@app.get("/credits", response_class=HTMLResponse) +async def ui() -> HTMLResponse: + return _render_frontend_page() + + +@app.get("/v1/models") +@app.get("/models") +async def list_models() -> dict[str, object]: + return { + "object": "list", + "data": [ + { + "id": model["id"], + "object": "model", + "created": int(time.time()), + "owned_by": "local", + } + for model in _supported_models() + ], + } + + +@app.get("/audio/models") +@app.get("/v1/audio/models") +async def list_audio_models() -> dict[str, list[dict[str, str]]]: + return {"models": _supported_models()} + + +@app.get("/audio/voices") +@app.get("/v1/audio/voices") +async def list_audio_voices() -> dict[str, list[dict[str, str]]]: + return {"voices": _supported_voices()} + + +@app.post("/audio/speech") +@app.post("/v1/audio/speech") +async def audio_speech(payload: SpeechRequest, request: Request) -> Response: + _ = request.headers.get("authorization") + request_id = getattr(request.state, "request_id", uuid4().hex[:12]) + request_source = getattr(request.state, "request_source", _guess_request_source(request)) + prepared = _prepare_request(payload) + voice_mode = ( + "local-reference" if prepared.voice_ref_audio_path is not None else "style-prompt" + ) + ref_text_source = ( + "request" + if payload.ref_text is not None + else "preset" + if prepared.voice_ref_text is not None + else "none" + ) + LOG.info( + "Prepared speech request (request_id=%s, source=%s, voice=%s, voice_mode=%s, language=%s, raw_chars=%d, sanitized_chars=%d, normalization=%s, ref_audio=%s, ref_text_source=%s, ref_text_chars=%d, preview_raw=%r, preview_sanitized=%r)", + request_id, + request_source, + prepared.voice_id, + voice_mode, + prepared.language, + len(payload.raw_text()), + len(prepared.text), + _normalization_summary(payload.normalization_options), + prepared.voice_ref_audio_path.name if prepared.voice_ref_audio_path is not None else "-", + ref_text_source, + len(prepared.voice_ref_text or prepared.ref_text or ""), + _truncate_preview(payload.raw_text()), + _truncate_preview(prepared.text), + ) + prepared, audio_bytes, media_type = await _synthesize_prepared( + prepared, + payload, + request_id=request_id, + ) + _, suffix = SUPPORTED_RESPONSE_FORMATS[prepared.response_format] + headers = { + "Content-Disposition": f'attachment; filename="speech.{suffix}"', + "X-OmniVoice-Text-Chunks": str(len(prepared.chunk_plan)), + "X-OmniVoice-Forced-Chunking": str(prepared.force_sentence_chunking).lower(), + "X-OmniVoice-Voice-Mode": voice_mode, + "X-OmniVoice-Sanitized-Chars": str(len(prepared.text)), + "X-OmniVoice-Language": prepared.language or "", + "X-OmniVoice-Voice-Source": ( + "local-reference" if prepared.voice_ref_audio_path is not None else "style-prompt" + ), + } + return Response(content=audio_bytes, media_type=media_type, headers=headers) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="OmniVoice OpenAI-compatible TTS server" + ) + parser.add_argument("--host", default=DEFAULT_HOST) + parser.add_argument("--port", type=int, default=DEFAULT_PORT) + parser.add_argument("--device", default=DEFAULT_DEVICE) + parser.add_argument("--model-id", default=BACKEND_MODEL_ID) + parser.add_argument( + "--idle-timeout", + type=float, + default=float(os.getenv("OMNIVOICE_IDLE_TIMEOUT", "300")), + help="Seconds of inactivity before offloading model from GPU (0 to disable)", + ) + return parser + + +def main() -> None: + _configure_logging() + args = build_parser().parse_args() + + global service + service = OmniVoiceService( + args.model_id, args.device, idle_timeout=args.idle_timeout + ) + + uvicorn.run( + app, + host=args.host, + port=args.port, + log_level="info", + access_log=True, + workers=1, + ) + + +if __name__ == "__main__": + main() diff --git a/omnivoice/utils/common.py b/omnivoice/utils/common.py index 6bdd3426..3d94b404 100644 --- a/omnivoice/utils/common.py +++ b/omnivoice/utils/common.py @@ -19,6 +19,7 @@ import argparse import random +from typing import Any import numpy as np import torch @@ -54,3 +55,95 @@ def fix_random_seed(random_seed: int): # Ensure deterministic ID creation rd = random.Random() rd.seed(random_seed) + + +def resolve_device_string(device: Any) -> str: + """Return a representative device string for runtime selection.""" + if device is None: + return "cpu" + if isinstance(device, dict): + values = [resolve_device_string(v) for v in device.values()] + for value in values: + if value.startswith("cuda"): + return value + for value in values: + if value.startswith("mps"): + return value + return values[0] if values else "cpu" + if isinstance(device, torch.device): + return str(device) + return str(device) + + +def get_best_device() -> str: + """Auto-detect the best available device: CUDA > MPS > CPU.""" + if torch.cuda.is_available(): + return "cuda" + if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +def get_best_device_and_count() -> tuple[str, int]: + """Return the best device string plus the number of usable devices.""" + device = get_best_device() + if device.startswith("cuda"): + return device, torch.cuda.device_count() + return device, 1 + + +def _get_cuda_device_index(device: Any) -> int: + normalized = torch.device(resolve_device_string(device)) + return normalized.index if normalized.index is not None else 0 + + +def _cuda_supports_bfloat16(device: Any) -> bool: + if not torch.cuda.is_available(): + return False + major, _ = torch.cuda.get_device_capability(_get_cuda_device_index(device)) + return major >= 8 + + +def resolve_inference_dtype(device: Any, dtype: Any = "auto") -> torch.dtype: + """Pick a safe and fast inference dtype for the requested device.""" + if isinstance(dtype, torch.dtype): + return dtype + + if dtype is None: + dtype = "auto" + + if not isinstance(dtype, str): + raise TypeError(f"Unsupported dtype specifier: {dtype!r}") + + normalized_dtype = dtype.lower() + if normalized_dtype == "auto": + device_str = resolve_device_string(device) + if device_str.startswith("cuda"): + return ( + torch.bfloat16 + if _cuda_supports_bfloat16(device_str) + else torch.float16 + ) + return torch.float32 + + aliases = { + "float16": torch.float16, + "fp16": torch.float16, + "half": torch.float16, + "bfloat16": torch.bfloat16, + "bf16": torch.bfloat16, + "float32": torch.float32, + "fp32": torch.float32, + } + if normalized_dtype not in aliases: + valid = ", ".join(sorted(aliases)) + raise ValueError(f"Unsupported dtype {dtype!r}. Expected one of: {valid}, auto") + return aliases[normalized_dtype] + + +def configure_cuda_inference(device: Any) -> None: + """Enable CUDA matmul fast paths that are safe for inference.""" + if not resolve_device_string(device).startswith("cuda"): + return + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True diff --git a/pyproject.toml b/pyproject.toml index c0f0c32e..c6a61321 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,10 @@ dependencies = [ "tensorboardX", "webdataset", "numpy", - "soundfile" + "soundfile", + "fastapi>=0.135.2", + "uvicorn>=0.42.0", + "inflect>=7.5.0", ] [project.optional-dependencies] @@ -58,6 +61,7 @@ eval = [ omnivoice-infer = "omnivoice.cli.infer:main" omnivoice-infer-batch = "omnivoice.cli.infer_batch:main" omnivoice-demo = "omnivoice.cli.demo:main" +omnivoice-openai-tts-server = "omnivoice.openai_tts_server:main" [project.urls] Homepage = "https://github.com/k2-fsa/OmniVoice" diff --git a/tests/test_inference_runtime.py b/tests/test_inference_runtime.py new file mode 100644 index 00000000..dd2508c0 --- /dev/null +++ b/tests/test_inference_runtime.py @@ -0,0 +1,51 @@ +import unittest +from unittest.mock import patch + +import torch + +from omnivoice.models.omnivoice import _build_block_mask_document_ids, _mask_mod_packed +from omnivoice.utils.common import resolve_inference_dtype + + +class InferenceRuntimeTests(unittest.TestCase): + def test_resolve_inference_dtype_prefers_bfloat16_on_ampere(self) -> None: + with ( + patch("torch.cuda.is_available", return_value=True), + patch("torch.cuda.get_device_capability", return_value=(8, 6)), + ): + self.assertEqual(resolve_inference_dtype("cuda:0"), torch.bfloat16) + + def test_resolve_inference_dtype_falls_back_to_float16_on_pre_ampere_cuda(self) -> None: + with ( + patch("torch.cuda.is_available", return_value=True), + patch("torch.cuda.get_device_capability", return_value=(7, 5)), + ): + self.assertEqual(resolve_inference_dtype("cuda:0"), torch.float16) + + def test_resolve_inference_dtype_uses_float32_off_cuda(self) -> None: + self.assertEqual(resolve_inference_dtype("cpu"), torch.float32) + self.assertEqual(resolve_inference_dtype("mps"), torch.float32) + + def test_build_block_mask_document_ids_keeps_padding_self_only(self) -> None: + document_ids = _build_block_mask_document_ids( + [4, 2], + max_seq_len=4, + device=torch.device("cpu"), + ) + expected = torch.tensor( + [ + [0, 0, 0, 0], + [0, 0, -1, -2], + ], + dtype=torch.int32, + ) + self.assertTrue(torch.equal(document_ids.cpu(), expected)) + + self.assertTrue(_mask_mod_packed(document_ids, 1, None, 0, 1)) + self.assertFalse(_mask_mod_packed(document_ids, 1, None, 0, 2)) + self.assertFalse(_mask_mod_packed(document_ids, 1, None, 2, 3)) + self.assertTrue(_mask_mod_packed(document_ids, 1, None, 2, 2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_openai_tts_server.py b/tests/test_openai_tts_server.py new file mode 100644 index 00000000..b7de5e2d --- /dev/null +++ b/tests/test_openai_tts_server.py @@ -0,0 +1,217 @@ +import unittest +from unittest.mock import AsyncMock, patch + +import torch +from fastapi.testclient import TestClient + +from omnivoice.openai_tts_server import ( + DEFAULT_AUDIO_CHUNK_THRESHOLD, + TextSanitizationOptions, + VOICE_LOOKUP, + _resolve_voice, + _supported_models, + _supported_voices, + app, + sanitize_prompt_text, + sanitize_speech_text, + service, +) + + +class _FakeModel: + def __init__(self) -> None: + self.sampling_rate = 24000 + self.calls: list[dict[str, object]] = [] + self.voice_prompt_calls: list[dict[str, object]] = [] + + def create_voice_clone_prompt(self, *, ref_audio: str, ref_text=None): + prompt = {"ref_audio": ref_audio, "ref_text": ref_text} + self.voice_prompt_calls.append(prompt) + return prompt + + def generate(self, **kwargs): + self.calls.append(kwargs) + return [torch.zeros(1, 480)] + + +class OpenAITTSServerTests(unittest.TestCase): + def setUp(self) -> None: + service._voice_prompt_cache.clear() + + def test_sanitize_speech_text_preserves_bracket_tags_and_removes_control_tokens( + self, + ) -> None: + text = " Hello\tworld <|text_end|>\n[laughter] me@example.com " + sanitized = sanitize_speech_text( + text, + language="en", + options=TextSanitizationOptions(), + ) + + self.assertNotIn("<|text_end|>", sanitized) + self.assertIn("[laughter]", sanitized) + self.assertIn("me at example dot com", sanitized) + self.assertEqual(sanitized, sanitized.strip()) + self.assertTrue(sanitized.endswith(".")) + + def test_sanitize_prompt_text_strips_model_tokens(self) -> None: + sanitized = sanitize_prompt_text(" <|lang_start|> male, low pitch \n") + self.assertEqual(sanitized, "male, low pitch") + + def test_sanitize_speech_text_applies_kokoro_style_normalization(self) -> None: + sanitized = sanitize_speech_text( + "Dr. Smith meets me at 10:05 pm. It costs $50.30 for 5km in 1998(s).", + language="en", + options=TextSanitizationOptions(unit_normalization=True), + ) + + self.assertIn("Doctor Smith", sanitized) + self.assertIn("ten oh five pm", sanitized) + self.assertIn("fifty dollars and thirty cents", sanitized) + self.assertIn("five kilometers", sanitized) + self.assertIn("nineteen ninety-eight", sanitized) + + def test_supported_models_and_voices_include_openwebui_facing_entries(self) -> None: + model_ids = {model["id"] for model in _supported_models()} + voice_ids = {voice["id"] for voice in _supported_voices()} + + self.assertIn("tts-1", model_ids) + self.assertIn("gpt-4o-mini-tts", model_ids) + self.assertIn("alloy", voice_ids) + self.assertIn("british_man", voice_ids) + + def test_resolve_voice_prefers_local_reference_when_available(self) -> None: + resolved = _resolve_voice("alloy") + option = VOICE_LOOKUP["alloy"] + + self.assertEqual(resolved.voice_id, "alloy") + if option.has_local_sample(): + self.assertIsNotNone(resolved.ref_audio_path) + self.assertIsNone(resolved.instruct) + self.assertTrue((resolved.ref_text or "").strip()) + else: + self.assertIsNone(resolved.ref_audio_path) + self.assertIsNotNone(resolved.instruct) + + def test_audio_endpoint_forces_sentence_chunking_for_long_input(self) -> None: + fake_model = _FakeModel() + long_text = " ".join(f"Sentence {idx}." for idx in range(1, 60)) + + with ( + patch.object(service, "get_model", new=AsyncMock(return_value=fake_model)), + patch( + "omnivoice.openai_tts_server._waveform_to_bytes", + return_value=(b"audio-bytes", "audio/mpeg"), + ), + TestClient(app) as client, + ): + response = client.post( + "/v1/audio/speech", + json={ + "input": long_text, + "voice": "alloy", + "response_format": "mp3", + }, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"audio-bytes") + self.assertGreater(int(response.headers["x-omnivoice-text-chunks"]), 1) + self.assertEqual(response.headers["x-omnivoice-forced-chunking"], "true") + + call = fake_model.calls[0] + generation_config = call["generation_config"] + self.assertEqual(generation_config.audio_chunk_threshold, 0.0) + self.assertTrue(call["text"].endswith(".")) + if VOICE_LOOKUP["alloy"].has_local_sample(): + self.assertIn("voice_clone_prompt", call) + self.assertTrue(call["voice_clone_prompt"]["ref_text"].strip()) + else: + self.assertIn("instruct", call) + + def test_audio_endpoint_keeps_default_chunk_threshold_for_short_input(self) -> None: + fake_model = _FakeModel() + + with ( + patch.object(service, "get_model", new=AsyncMock(return_value=fake_model)), + patch( + "omnivoice.openai_tts_server._waveform_to_bytes", + return_value=(b"audio-bytes", "audio/mpeg"), + ), + TestClient(app) as client, + ): + response = client.post( + "/v1/audio/speech", + json={ + "input": "Hello world", + "voice": "alloy", + "response_format": "mp3", + }, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["x-omnivoice-text-chunks"], "1") + self.assertEqual(response.headers["x-omnivoice-forced-chunking"], "false") + + call = fake_model.calls[0] + generation_config = call["generation_config"] + self.assertEqual(generation_config.audio_chunk_threshold, DEFAULT_AUDIO_CHUNK_THRESHOLD) + if VOICE_LOOKUP["alloy"].has_local_sample(): + self.assertIn("voice_clone_prompt", call) + self.assertTrue(call["voice_clone_prompt"]["ref_text"].strip()) + + def test_audio_endpoint_allows_ref_text_override_for_local_voice(self) -> None: + fake_model = _FakeModel() + + with ( + patch.object(service, "get_model", new=AsyncMock(return_value=fake_model)), + patch( + "omnivoice.openai_tts_server._waveform_to_bytes", + return_value=(b"audio-bytes", "audio/mpeg"), + ), + TestClient(app) as client, + ): + response = client.post( + "/v1/audio/speech", + json={ + "input": "Hello world", + "voice": "alloy", + "ref_text": "Custom reference sentence.", + "response_format": "mp3", + }, + ) + + self.assertEqual(response.status_code, 200) + if VOICE_LOOKUP["alloy"].has_local_sample(): + self.assertEqual( + fake_model.calls[0]["voice_clone_prompt"]["ref_text"], + "Custom reference sentence.", + ) + + def test_audio_endpoint_validates_speed_bounds(self) -> None: + with TestClient(app) as client: + response = client.post( + "/v1/audio/speech", + json={ + "input": "Hello world", + "voice": "alloy", + "speed": 4.5, + }, + ) + + self.assertEqual(response.status_code, 422) + + def test_frontend_page_shows_credits_and_voice_summary(self) -> None: + with TestClient(app) as client: + response = client.get("/ui") + + self.assertEqual(response.status_code, 200) + self.assertIn("text/html", response.headers["content-type"]) + self.assertIn("Thanks to the original OmniVoice creators", response.text) + self.assertIn("OpenAI-compatible TTS", response.text) + self.assertIn("british_man", response.text) + self.assertIn("cordobes_man", response.text) + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 8d41c1a8..59faf0da 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", @@ -25,16 +25,16 @@ constraints = [ [[package]] name = "accelerate" version = "1.13.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } @@ -45,7 +45,7 @@ wheels = [ [[package]] name = "aiofiles" version = "24.1.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, @@ -54,7 +54,7 @@ wheels = [ [[package]] name = "aliyun-python-sdk-core" version = "2.16.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jmespath" }, @@ -64,7 +64,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97 [[package]] name = "aliyun-python-sdk-kms" version = "2.16.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aliyun-python-sdk-core" }, ] @@ -76,7 +76,7 @@ wheels = [ [[package]] name = "annotated-doc" version = "0.0.4" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, @@ -85,7 +85,7 @@ wheels = [ [[package]] name = "annotated-types" version = "0.7.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, @@ -94,13 +94,13 @@ wheels = [ [[package]] name = "antlr4-python3-runtime" version = "4.9.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } [[package]] name = "anyio" version = "4.12.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, @@ -114,7 +114,7 @@ wheels = [ [[package]] name = "audioop-lts" version = "0.2.2" -source = { registry = "https://pypi.org/simple/" } +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" }, @@ -170,7 +170,7 @@ wheels = [ [[package]] name = "audioread" version = "3.1.0" -source = { registry = "https://pypi.org/simple/" } +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'" }, @@ -183,7 +183,7 @@ wheels = [ [[package]] name = "braceexpand" version = "0.1.7" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/93/badd4f5ccf25209f3fef2573073da9fe4a45a3da99fca2f800f942130c0f/braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705", size = 7777, upload-time = "2021-05-07T13:49:07.323Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/93/e8c04e80e82391a6e51f218ca49720f64236bc824e92152a2633b74cf7ab/braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014", size = 5923, upload-time = "2021-05-07T13:49:05.146Z" }, @@ -192,7 +192,7 @@ wheels = [ [[package]] name = "brotli" version = "1.2.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, @@ -250,7 +250,7 @@ wheels = [ [[package]] name = "certifi" version = "2026.2.25" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, @@ -259,7 +259,7 @@ wheels = [ [[package]] name = "cffi" version = "2.0.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] @@ -341,7 +341,7 @@ wheels = [ [[package]] name = "charset-normalizer" version = "3.4.6" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, @@ -446,7 +446,7 @@ wheels = [ [[package]] name = "click" version = "8.3.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] @@ -458,7 +458,7 @@ wheels = [ [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, @@ -467,13 +467,13 @@ wheels = [ [[package]] name = "crcmod" version = "1.7" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } [[package]] name = "cryptography" version = "46.0.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, @@ -533,7 +533,7 @@ wheels = [ [[package]] name = "decorator" version = "5.2.1" -source = { registry = "https://pypi.org/simple/" } +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" }, @@ -542,7 +542,7 @@ wheels = [ [[package]] name = "editdistance" version = "0.8.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz", hash = "sha256:d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed", size = 50006, upload-time = "2024-02-10T07:44:53.914Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/09/c9/302658ce7f4c537a4e85cf578d11bbf7af120a712e1d78fedc6cb8823c65/editdistance-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:adeb705f32b93accc74960d227875abff150ee42d676e428536361fe5f8f5388", size = 106150, upload-time = "2024-02-10T07:43:15.903Z" }, @@ -588,7 +588,7 @@ wheels = [ [[package]] name = "exceptiongroup" version = "1.3.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -600,7 +600,7 @@ wheels = [ [[package]] name = "fastapi" version = "0.135.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, @@ -616,7 +616,7 @@ wheels = [ [[package]] name = "ffmpy" version = "1.0.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" }, @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "filelock" version = "3.25.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, @@ -634,7 +634,7 @@ wheels = [ [[package]] name = "fsspec" version = "2026.2.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, @@ -643,7 +643,7 @@ wheels = [ [[package]] name = "funasr" version = "1.3.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "editdistance" }, { name = "hydra-core" }, @@ -657,8 +657,8 @@ dependencies = [ { name = "pytorch-wpe" }, { name = "pyyaml" }, { name = "requests" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sentencepiece" }, { name = "soundfile" }, { name = "tensorboardx" }, @@ -674,7 +674,7 @@ wheels = [ [[package]] name = "gradio" version = "6.9.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, { name = "anyio" }, @@ -688,12 +688,12 @@ dependencies = [ { name = "huggingface-hub" }, { name = "jinja2" }, { name = "markupsafe" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "orjson" }, { name = "packaging" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "pydantic" }, { name = "pydub" }, @@ -716,7 +716,7 @@ wheels = [ [[package]] name = "gradio-client" version = "2.3.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fsspec" }, { name = "httpx" }, @@ -732,7 +732,7 @@ wheels = [ [[package]] name = "groovy" version = "0.1.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, @@ -741,7 +741,7 @@ wheels = [ [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, @@ -750,7 +750,7 @@ wheels = [ [[package]] name = "hf-xet" version = "1.4.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, @@ -782,7 +782,7 @@ wheels = [ [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, @@ -795,7 +795,7 @@ wheels = [ [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, @@ -810,7 +810,7 @@ wheels = [ [[package]] name = "huggingface-hub" version = "1.7.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, @@ -830,7 +830,7 @@ wheels = [ [[package]] name = "hydra-core" version = "1.3.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "antlr4-python3-runtime" }, { name = "omegaconf" }, @@ -844,16 +844,29 @@ wheels = [ [[package]] name = "idna" version = "3.11" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + [[package]] name = "jaconv" version = "0.5.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/0e/9fffaacda59bdfa479372c71d18d72968d2af5a36a5a2086b02a60124b98/jaconv-0.5.0.tar.gz", hash = "sha256:53f6f968276846716f0f37100a6d5c7308cfa1e0c714eb41287d5bb09345c40f", size = 21816, upload-time = "2026-02-08T11:15:57.07Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/da/9657d637bcacdbaf6a914ce504000da5639f9d945f8d3552a940f021d6c0/jaconv-0.5.0-py3-none-any.whl", hash = "sha256:2914114fe761ca49fc7089e25e6ad4a400c26f262ffce84e13b176916b71610a", size = 16831, upload-time = "2026-02-08T11:15:55.322Z" }, @@ -862,7 +875,7 @@ wheels = [ [[package]] name = "jamo" version = "0.4.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b1/a2/bda770579809726e929ca6356743f9f50f64a2cbaee578fa9d4824afb00e/jamo-0.4.1.tar.gz", hash = "sha256:ea65cf9d35338d0e0af48d75ff426d8a369b0ebde6f07051c3ac37256f56d025", size = 7386, upload-time = "2017-11-06T19:28:51.729Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ac/cc/49812faae67f9a24be6ddaf58a2cf7e8c3cbfcf5b762d9414f7103d2ea2c/jamo-0.4.1-py3-none-any.whl", hash = "sha256:d4b94fd23324c606ed2fbc4037c603e2c3a7ae9390c05d3473aea1ccb6b1c3fb", size = 9543, upload-time = "2017-11-06T19:28:49.624Z" }, @@ -871,13 +884,13 @@ wheels = [ [[package]] name = "jieba" version = "0.42.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] @@ -889,7 +902,7 @@ wheels = [ [[package]] name = "jiwer" version = "3.1.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rapidfuzz" }, @@ -902,7 +915,7 @@ wheels = [ [[package]] name = "jmespath" version = "0.10.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, @@ -911,7 +924,7 @@ wheels = [ [[package]] name = "joblib" version = "1.5.3" -source = { registry = "https://pypi.org/simple/" } +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" }, @@ -920,10 +933,10 @@ wheels = [ [[package]] name = "kaldiio" version = "2.18.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/85/92435e8e62eb3d43eded9f24643fc2a6dbce031cebceed11528147c7873f/kaldiio-2.18.1.tar.gz", hash = "sha256:0283d197fac6ac683f7a9e6af8d18aad9dbd2c4a997f22e45294f2ac1ee3c432", size = 35570, upload-time = "2025-03-06T15:57:52.375Z" } wheels = [ @@ -933,7 +946,7 @@ wheels = [ [[package]] name = "lazy-loader" version = "0.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] @@ -945,7 +958,7 @@ wheels = [ [[package]] name = "librosa" version = "0.11.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audioread" }, { name = "decorator" }, @@ -953,13 +966,13 @@ dependencies = [ { name = "lazy-loader" }, { name = "msgpack" }, { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pooch" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "soundfile" }, { name = "soxr" }, { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, @@ -974,7 +987,7 @@ wheels = [ [[package]] name = "llvmlite" version = "0.46.0" -source = { registry = "https://pypi.org/simple/" } +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/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, @@ -1002,7 +1015,7 @@ wheels = [ [[package]] name = "markdown-it-py" version = "4.0.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] @@ -1014,7 +1027,7 @@ wheels = [ [[package]] name = "markupsafe" version = "3.0.3" -source = { registry = "https://pypi.org/simple/" } +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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, @@ -1099,7 +1112,7 @@ wheels = [ [[package]] name = "mdurl" version = "0.1.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, @@ -1108,7 +1121,7 @@ wheels = [ [[package]] name = "modelscope" version = "1.35.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "packaging" }, @@ -1122,10 +1135,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/d3/c98f736bbb5739871214e567ef9a1f5fca65f10d1b7bdc5e1bd565d492cf/modelscope-1.35.1-py3-none-any.whl", hash = "sha256:364db742867988da6be0493e0b9c4fd3e13bb0f5dd230c0c928102775aeed375", size = 6053743, upload-time = "2026-03-19T06:52:59.37Z" }, ] +[[package]] +name = "more-itertools" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/24/e0acc4bf54cba50c1d432c70a72a3df96db4a321b2c4c68432a60759044f/more_itertools-11.0.1.tar.gz", hash = "sha256:fefaf25b7ab08f0b45fa9f1892cae93b9fc0089ef034d39213bce15f1cc9e199", size = 144739, upload-time = "2026-04-02T16:17:45.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/f4/5e52c7319b8087acef603ed6e50dc325c02eaa999355414830468611f13c/more_itertools-11.0.1-py3-none-any.whl", hash = "sha256:eaf287826069452a8f61026c597eae2428b2d1ba2859083abbf240b46842ce6d", size = 72182, upload-time = "2026-04-02T16:17:43.724Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } 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" }, @@ -1134,7 +1156,7 @@ wheels = [ [[package]] name = "msgpack" version = "1.1.2" -source = { registry = "https://pypi.org/simple/" } +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/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, @@ -1195,7 +1217,7 @@ wheels = [ [[package]] name = "networkx" version = "3.4.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')", "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", @@ -1208,7 +1230,7 @@ wheels = [ [[package]] name = "networkx" version = "3.6.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", @@ -1227,11 +1249,11 @@ wheels = [ [[package]] name = "numba" version = "0.64.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llvmlite" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/c9/a0fb41787d01d621046138da30f6c2100d80857bf34b3390dd68040f27a3/numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1", size = 2765679, upload-time = "2026-02-18T18:41:20.974Z" } wheels = [ @@ -1260,7 +1282,7 @@ wheels = [ [[package]] name = "numpy" version = "2.2.6" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')", "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", @@ -1326,7 +1348,7 @@ wheels = [ [[package]] name = "numpy" version = "2.4.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", @@ -1415,7 +1437,7 @@ wheels = [ [[package]] name = "nvidia-cublas-cu12" version = "12.8.4.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, ] @@ -1423,7 +1445,7 @@ wheels = [ [[package]] name = "nvidia-cuda-cupti-cu12" version = "12.8.90" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, ] @@ -1431,7 +1453,7 @@ wheels = [ [[package]] name = "nvidia-cuda-nvrtc-cu12" version = "12.8.93" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, ] @@ -1439,7 +1461,7 @@ wheels = [ [[package]] name = "nvidia-cuda-runtime-cu12" version = "12.8.90" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, ] @@ -1447,7 +1469,7 @@ wheels = [ [[package]] name = "nvidia-cudnn-cu12" version = "9.10.2.21" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -1458,7 +1480,7 @@ wheels = [ [[package]] name = "nvidia-cufft-cu12" version = "11.3.3.83" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -1469,7 +1491,7 @@ wheels = [ [[package]] name = "nvidia-cufile-cu12" version = "1.13.1.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, ] @@ -1477,7 +1499,7 @@ wheels = [ [[package]] name = "nvidia-curand-cu12" version = "10.3.9.90" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, ] @@ -1485,7 +1507,7 @@ wheels = [ [[package]] name = "nvidia-cusolver-cu12" version = "11.7.3.90" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1498,7 +1520,7 @@ wheels = [ [[package]] name = "nvidia-cusparse-cu12" version = "12.5.8.93" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -1509,7 +1531,7 @@ wheels = [ [[package]] name = "nvidia-cusparselt-cu12" version = "0.7.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, ] @@ -1517,7 +1539,7 @@ wheels = [ [[package]] name = "nvidia-nccl-cu12" version = "2.27.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" }, ] @@ -1525,7 +1547,7 @@ wheels = [ [[package]] name = "nvidia-nvjitlink-cu12" version = "12.8.93" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, ] @@ -1533,7 +1555,7 @@ wheels = [ [[package]] name = "nvidia-nvtx-cu12" version = "12.8.90" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, ] @@ -1541,7 +1563,7 @@ wheels = [ [[package]] name = "omegaconf" version = "2.3.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "antlr4-python3-runtime" }, { name = "pyyaml" }, @@ -1557,17 +1579,20 @@ version = "0.1.3" source = { editable = "." } dependencies = [ { name = "accelerate" }, + { name = "fastapi" }, { name = "gradio" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "inflect" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pydub" }, { name = "soundfile" }, { name = "tensorboardx" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torchaudio", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "transformers" }, + { name = "uvicorn" }, { name = "webdataset" }, ] @@ -1585,8 +1610,10 @@ eval = [ [package.metadata] requires-dist = [ { name = "accelerate" }, + { name = "fastapi", specifier = ">=0.135.2" }, { name = "funasr", marker = "extra == 'eval'" }, { name = "gradio" }, + { name = "inflect", specifier = ">=7.5.0" }, { name = "jiwer", marker = "extra == 'eval'", specifier = "==3.1.0" }, { name = "librosa", marker = "extra == 'eval'" }, { name = "numpy" }, @@ -1600,6 +1627,7 @@ requires-dist = [ { name = "torchaudio", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = ">=2.4", index = "https://download.pytorch.org/whl/cu128" }, { name = "transformers", specifier = ">=5.3.0" }, { name = "unidecode", marker = "extra == 'eval'" }, + { name = "uvicorn", specifier = ">=0.42.0" }, { name = "webdataset" }, { name = "zhconv", marker = "extra == 'eval'" }, { name = "zhon", marker = "extra == 'eval'" }, @@ -1609,7 +1637,7 @@ provides-extras = ["eval"] [[package]] name = "orjson" version = "3.11.7" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, @@ -1690,7 +1718,7 @@ wheels = [ [[package]] name = "oss2" version = "2.19.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aliyun-python-sdk-core" }, { name = "aliyun-python-sdk-kms" }, @@ -1704,7 +1732,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac22 [[package]] name = "packaging" version = "26.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, @@ -1713,13 +1741,13 @@ wheels = [ [[package]] name = "pandas" version = "2.3.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')", "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "python-dateutil", marker = "python_full_version < '3.11'" }, { name = "pytz", marker = "python_full_version < '3.11'" }, { name = "tzdata", marker = "python_full_version < '3.11'" }, @@ -1778,7 +1806,7 @@ wheels = [ [[package]] name = "pandas" version = "3.0.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", @@ -1790,7 +1818,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] @@ -1848,7 +1876,7 @@ wheels = [ [[package]] name = "pillow" version = "12.1.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, @@ -1946,7 +1974,7 @@ wheels = [ [[package]] name = "platformdirs" version = "4.9.4" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, @@ -1955,7 +1983,7 @@ wheels = [ [[package]] name = "pooch" version = "1.9.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1969,7 +1997,7 @@ wheels = [ [[package]] name = "protobuf" version = "7.34.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, @@ -1984,7 +2012,7 @@ wheels = [ [[package]] name = "psutil" version = "7.2.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, @@ -2012,7 +2040,7 @@ wheels = [ [[package]] name = "pycparser" version = "3.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, @@ -2021,7 +2049,7 @@ wheels = [ [[package]] name = "pycryptodome" version = "3.23.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, @@ -2056,7 +2084,7 @@ wheels = [ [[package]] name = "pydantic" version = "2.12.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, @@ -2071,7 +2099,7 @@ wheels = [ [[package]] name = "pydantic-core" version = "2.41.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] @@ -2189,7 +2217,7 @@ wheels = [ [[package]] name = "pydub" version = "0.25.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, @@ -2198,7 +2226,7 @@ wheels = [ [[package]] name = "pygments" version = "2.19.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, @@ -2207,15 +2235,15 @@ wheels = [ [[package]] name = "pynndescent" version = "0.6.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, { name = "llvmlite" }, { name = "numba" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } wheels = [ @@ -2225,7 +2253,7 @@ wheels = [ [[package]] name = "python-dateutil" version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] @@ -2237,7 +2265,7 @@ wheels = [ [[package]] name = "python-multipart" version = "0.0.22" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, @@ -2246,10 +2274,10 @@ wheels = [ [[package]] name = "pytorch-wpe" version = "0.0.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/39/8d94737fd6fab4028687575099566a125100f3ba8c638f861506747d7b7c/pytorch_wpe-0.0.1.tar.gz", hash = "sha256:fc7e706b5411800c4483fe94db7dcd82ecf6c57bc013af529ab4fb675c9cc29c", size = 4457, upload-time = "2021-03-05T10:10:09.593Z" } wheels = [ @@ -2259,7 +2287,7 @@ wheels = [ [[package]] name = "pytz" version = "2026.1.post1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, @@ -2268,7 +2296,7 @@ wheels = [ [[package]] name = "pyyaml" version = "6.0.3" -source = { registry = "https://pypi.org/simple/" } +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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, @@ -2332,7 +2360,7 @@ wheels = [ [[package]] name = "rapidfuzz" version = "3.14.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, @@ -2422,7 +2450,7 @@ wheels = [ [[package]] name = "regex" version = "2026.2.28" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, @@ -2543,7 +2571,7 @@ wheels = [ [[package]] name = "requests" version = "2.32.5" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, @@ -2558,7 +2586,7 @@ wheels = [ [[package]] name = "rich" version = "14.3.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, @@ -2571,19 +2599,19 @@ wheels = [ [[package]] name = "s3prl" version = "0.4.18" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "omegaconf" }, { name = "protobuf" }, { name = "pyyaml" }, { name = "requests" }, { name = "setuptools" }, - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torchaudio", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tqdm" }, { name = "transformers" }, @@ -2593,7 +2621,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/c6/7f/de69806654326a958 [[package]] name = "safehttpx" version = "0.1.7" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, ] @@ -2605,7 +2633,7 @@ wheels = [ [[package]] name = "safetensors" version = "0.7.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, @@ -2631,15 +2659,15 @@ wheels = [ [[package]] name = "scikit-learn" version = "1.7.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')", "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } @@ -2679,7 +2707,7 @@ wheels = [ [[package]] name = "scikit-learn" version = "1.8.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", @@ -2692,8 +2720,8 @@ resolution-markers = [ ] dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] 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" } @@ -2739,13 +2767,13 @@ wheels = [ [[package]] name = "scipy" version = "1.15.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')", "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -2799,7 +2827,7 @@ wheels = [ [[package]] name = "scipy" version = "1.17.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')", "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')", @@ -2811,7 +2839,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -2880,7 +2908,7 @@ wheels = [ [[package]] name = "semantic-version" version = "2.10.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, @@ -2889,7 +2917,7 @@ wheels = [ [[package]] name = "sentencepiece" version = "0.2.1" -source = { registry = "https://pypi.org/simple/" } +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/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, @@ -2953,7 +2981,7 @@ wheels = [ [[package]] name = "setuptools" version = "82.0.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, @@ -2962,7 +2990,7 @@ wheels = [ [[package]] name = "shellingham" version = "1.5.4" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } 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" }, @@ -2971,7 +2999,7 @@ wheels = [ [[package]] name = "six" version = "1.17.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, @@ -2980,11 +3008,11 @@ wheels = [ [[package]] name = "soundfile" version = "0.13.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] 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 = [ @@ -3000,10 +3028,10 @@ wheels = [ [[package]] name = "soxr" version = "1.0.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] 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 = [ @@ -3032,7 +3060,7 @@ wheels = [ [[package]] name = "standard-aifc" version = "3.13.0" -source = { registry = "https://pypi.org/simple/" } +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'" }, @@ -3045,7 +3073,7 @@ wheels = [ [[package]] name = "standard-chunk" version = "3.13.0" -source = { registry = "https://pypi.org/simple/" } +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" }, @@ -3054,7 +3082,7 @@ wheels = [ [[package]] name = "standard-sunau" version = "3.13.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, ] @@ -3066,7 +3094,7 @@ wheels = [ [[package]] name = "starlette" version = "0.52.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, @@ -3079,7 +3107,7 @@ wheels = [ [[package]] name = "sympy" version = "1.14.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] @@ -3091,10 +3119,10 @@ wheels = [ [[package]] name = "tensorboardx" version = "2.6.4" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "protobuf" }, ] @@ -3106,7 +3134,7 @@ wheels = [ [[package]] name = "threadpoolctl" version = "3.6.0" -source = { registry = "https://pypi.org/simple/" } +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" }, @@ -3115,7 +3143,7 @@ wheels = [ [[package]] name = "tokenizers" version = "0.22.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] @@ -3145,7 +3173,7 @@ wheels = [ [[package]] name = "tomlkit" version = "0.13.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, @@ -3154,7 +3182,7 @@ wheels = [ [[package]] name = "torch" version = "2.8.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", @@ -3166,8 +3194,8 @@ dependencies = [ { name = "filelock", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "fsspec", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "jinja2", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "sympy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, @@ -3195,8 +3223,8 @@ dependencies = [ { name = "filelock", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "fsspec", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jinja2", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple/" }, marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple/" }, marker = "(python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -3232,10 +3260,10 @@ wheels = [ [[package]] name = "torch-complex" version = "0.4.4" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/17cb15a383cf2135330371e034d13b9043dc6d8bd07c871b5aa3064fbed1/torch_complex-0.4.4.tar.gz", hash = "sha256:4153fd6b24a0bad689e6f193bfbd00f38283b1890d808bef684ddc6d1f63fd3f", size = 10025, upload-time = "2024-06-28T07:10:28.136Z" } @@ -3246,7 +3274,7 @@ wheels = [ [[package]] name = "torchaudio" version = "2.8.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'", @@ -3255,7 +3283,7 @@ resolution-markers = [ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'", ] dependencies = [ - { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/30/81/92d34ff136b17ddda872f6d8149f2ca927ad53a37ae26d02cb5f66435772/torchaudio-2.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c2f44cf279f673cfcdd8f576c349eee8bedf8caab351a5dd78b32970cc34a212", size = 1852315, upload-time = "2025-08-06T14:58:32.64Z" }, @@ -3295,7 +3323,7 @@ wheels = [ [[package]] name = "tqdm" version = "4.67.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] @@ -3307,11 +3335,11 @@ wheels = [ [[package]] name = "transformers" version = "5.3.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -3328,7 +3356,7 @@ wheels = [ [[package]] name = "triton" version = "3.4.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -3340,10 +3368,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/63/8cb444ad5cdb25d999b7d647abac25af0ee37d292afc009940c05b82dda0/triton-3.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7936b18a3499ed62059414d7df563e6c163c5e16c3773678a3ee3d417865035d", size = 155659780, upload-time = "2025-07-30T19:58:51.171Z" }, ] +[[package]] +name = "typeguard" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, +] + [[package]] name = "typer" version = "0.24.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "click" }, @@ -3358,7 +3398,7 @@ wheels = [ [[package]] name = "typing-extensions" version = "4.15.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, @@ -3367,7 +3407,7 @@ wheels = [ [[package]] name = "typing-inspection" version = "0.4.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] @@ -3379,7 +3419,7 @@ wheels = [ [[package]] name = "tzdata" version = "2025.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, @@ -3388,16 +3428,16 @@ wheels = [ [[package]] name = "umap-learn" version = "0.5.11" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pynndescent" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/9a/a1e4a257a9aa979dac4f6d5781dac929cbb0949959e2003ed82657d10b0f/umap_learn-0.5.11.tar.gz", hash = "sha256:31566ffd495fbf05d7ab3efcba703861c0f5e6fc6998a838d0e2becdd00e54f5", size = 96409, upload-time = "2026-01-12T20:44:47.553Z" } @@ -3408,7 +3448,7 @@ wheels = [ [[package]] name = "unidecode" version = "1.4.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, @@ -3417,7 +3457,7 @@ wheels = [ [[package]] name = "urllib3" version = "2.6.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, @@ -3426,7 +3466,7 @@ wheels = [ [[package]] name = "uvicorn" version = "0.42.0" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, @@ -3440,11 +3480,11 @@ wheels = [ [[package]] name = "webdataset" version = "1.0.2" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "braceexpand" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/3a/68800d92e065cf4750ebecf973b13979c0c929b439e1293012938862038d/webdataset-1.0.2.tar.gz", hash = "sha256:7f0498be827cfa46cc5430a58768a24e2c6a410676a61be1838f53d61afdaab4", size = 80090, upload-time = "2025-06-19T23:26:21.945Z" } @@ -3455,13 +3495,13 @@ wheels = [ [[package]] name = "zhconv" version = "1.4.3" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/25/47/c8ae2d5d4025e253211ff3d8c163f457db1da94976cb582337a5ab76cb87/zhconv-1.4.3.tar.gz", hash = "sha256:ad42d9057ca0605f8e41d62b67ca797f879f58193ee6840562c51459b2698c45", size = 211571, upload-time = "2021-10-31T05:59:44.096Z" } [[package]] name = "zhon" version = "2.1.1" -source = { registry = "https://pypi.org/simple/" } +source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2a/8d/e6c8ee4df75940ddecb6f5e7b2753a9a3914e6cd357e7238987661723d81/zhon-2.1.1.tar.gz", hash = "sha256:c8da424fad4aa698ddd3e1735515b49ff522c84dd1091e8fafd986ac7c6898f0", size = 84448, upload-time = "2024-11-20T00:29:10.119Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/13/b8d6ca3a41bf5959bc3915678837385bf668162a7295140d81ccae0b96b7/zhon-2.1.1-py3-none-any.whl", hash = "sha256:9ec0d42229ae60708a045270f755ccefb48f2689f2a6ce0bc6597abf6c4a9871", size = 83933, upload-time = "2024-11-20T00:29:08.209Z" },