From 5e0434bd91dc232efb652c7bdeca04c7b5ac2ebb Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 01:03:02 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20CrispASR=20?= =?UTF-8?q?=E5=BC=95=E6=93=8E=E5=92=8C=20uv=20=E4=BE=9D=E8=B5=96=E9=94=81?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 CrispASR worker 后端, 支持 GGUF/bin 单文件模型、HuggingFace 下载缓存、本地模型扫描、GPU backend/设备/标点/统一内存配置, 并在设置面板暴露模型下载和缓存状态。 安装和发布流程改为通过 uv、pyproject.toml、uv.lock 同步依赖, 单独安装匹配的 PyTorch/FunASR, 并为 CrispASR 下载对应 Windows native runtime; CUDA runtime 安装失败时自动回退 CPU runtime。 同步更新默认配置、README/CLAUDE 文档、中英文 changelog 和 i18n 文案, 删除旧 requirements.txt。 --- CLAUDE.md | 4 + README.md | 18 +- README_zh.md | 18 +- asr_crispasr.py | 332 +++++++ asr_worker.py | 24 + build_release.ps1 | 64 +- config.yaml | 9 +- control_panel.py | 301 +++++- dialogs.py | 8 + i18n/CHANGELOG_en.md | 6 + i18n/CHANGELOG_zh.md | 6 + i18n/en.yaml | 12 + i18n/zh.yaml | 12 + install.ps1 | 166 +++- main.py | 144 ++- model_manager.py | 239 ++++- pyproject.toml | 36 + requirements.txt | 40 - update.bat | 24 +- uv.lock | 2106 ++++++++++++++++++++++++++++++++++++++++++ 20 files changed, 3444 insertions(+), 125 deletions(-) create mode 100644 asr_crispasr.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/CLAUDE.md b/CLAUDE.md index 913be9d..1e69787 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,6 +32,7 @@ main.py (LiveTranslateApp) |-- asr_sensevoice.py FunASR SenseVoice backend (better for Japanese) |-- asr_funasr_nano.py FunASR Nano backend |-- asr_anime_whisper.py Anime-Whisper backend (litagin/anime-whisper, ja anime/galgame) + |-- asr_crispasr.py CrispASR ggml runtime backend (GGUF/bin single-file models) |-- translator.py OpenAI-compatible API client, streaming, JSON schema, context history |-- subtitle_overlay.py PyQt6 transparent overlay (2-row header: controls + model/lang combos) |-- subtitle_window.py Standalone subtitle window for OBS capture (outlined text, animations) @@ -168,6 +169,9 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr - Audio chunk duration is 32ms (512 samples at 16kHz), matching Silero VAD's native window size for minimal latency - FunASR `disable_pbar=True` required in all `generate()` calls — tqdm crashes in GUI process when flushing stderr - ASR engine lifecycle: the GUI process never instantiates `ASREngine`, `FunASREngine`, or `AnimeWhisperEngine` directly. It owns an `ASRClient`; each worker process owns one concrete backend/model. Engine/model/device changes shut down the current worker first, then start a new worker. On target load failure, the saved previous worker config is used to restore ASR. +- CrispASR is treated as a ggml C++ runtime hub with GGUF/bin single-file weights. The GUI process never imports `crispasr`; `asr_crispasr.py` imports the Python binding only inside the ASR worker process and adapts results to the common ASR dict. +- CrispASR packaging has two parts that must stay aligned: `pyproject.toml` installs the pure-Python binding from the CrispASR release tag via `uv sync`, while `install.ps1` and the portable `bootstrap.ps1` download the matching prebuilt Windows `libcrispasr` DLL runtime from GitHub Releases. Do not replace this with plain `crispasr` from PyPI; that package name may not exist in the configured registry and the binding alone does not include `crispasr.dll`. +- CrispASR native runtime install prefers `libcrispasr-windows-x86_64-cuda.tar.gz` on NVIDIA systems and falls back to `libcrispasr-windows-x86_64.tar.gz`. DLLs are copied next to the installed `crispasr` package so the worker can load them without a global PATH edit. - Whisper (ctranslate2) only accepts `device="cuda"` not `"cuda:0"`; device index passed via `device_index` param. Parsed from combo text like `"cuda:0 (RTX 4090)"` in `_switch_asr_engine` - ASR text density filter: segments ≥2s producing ≤3 alnum characters are discarded as noise - Settings file uses atomic write (write to `.tmp` then `os.replace`) to prevent corruption on crash diff --git a/README.md b/README.md index b129b87..cf0a492 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Works with any system audio — videos, livestreams, voice chat. No player modif ## Features - **Real-time pipeline**: System audio → VAD → ASR → LLM translation → overlay -- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper +- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper, CrispASR - **Any OpenAI-compatible API**: DeepSeek, Grok, Qwen, GPT, Ollama, vLLM, etc. - **Streaming translation display**: Real-time character-by-character translation output - **Per-model settings**: Streaming, structured output (JSON), context history, disable thinking @@ -70,23 +70,23 @@ To update, double-click **`update.bat`** — it will pull the latest code and up Manual install ```bash -python -m venv .venv +uv venv --python 3.12 .venv .venv\Scripts\activate # PyTorch (choose one) -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu126 # CUDA -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 # CUDA (RTX 50xx) -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # CPU only +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu126 # CUDA +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 # CUDA (RTX 50xx) +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # CPU only # Dependencies -pip install -r requirements.txt -pip install funasr --no-deps +uv sync --locked --inexact --no-install-package torch --no-install-package torchaudio +uv pip install funasr --no-deps # Launch .venv\Scripts\python.exe main.py ``` -> FunASR uses `--no-deps` because `editdistance` requires a C++ compiler. `editdistance-s` in `requirements.txt` is a pure-Python drop-in replacement. +> FunASR uses `--no-deps` because `editdistance` requires a C++ compiler. `editdistance-s` in `pyproject.toml` is a pure-Python drop-in replacement. @@ -123,6 +123,7 @@ main.py Entry point & pipeline ├── asr_sensevoice.py SenseVoice backend ├── asr_funasr_nano.py FunASR Nano backend ├── asr_anime_whisper.py Anime-Whisper backend (ja anime/galgame) +├── asr_crispasr.py CrispASR ggml runtime backend ├── translator.py OpenAI-compatible client (streaming, JSON schema, context) ├── model_manager.py Model download & cache ├── subtitle_overlay.py PyQt6 overlay @@ -136,6 +137,7 @@ main.py Entry point & pipeline - [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — Whisper inference via CTranslate2 - [FunASR](https://github.com/modelscope/FunASR) — SenseVoice / Fun-ASR-Nano - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — Japanese anime/galgame ASR +- CrispASR — ggml C++ ASR runtime hub with GGUF/bin single-file models, used through its Python binding in the ASR worker - [Silero VAD](https://github.com/snakers4/silero-vad) — Voice activity detection ## Star History diff --git a/README_zh.md b/README_zh.md index edeb32d..550f628 100644 --- a/README_zh.md +++ b/README_zh.md @@ -21,7 +21,7 @@ Windows 实时音频翻译工具。捕获系统音频(WASAPI loopback)和可 ## 功能特性 - **实时翻译管线**:系统音频 → VAD → ASR → LLM 翻译 → 字幕显示 -- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper +- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper、CrispASR - **兼容任意 OpenAI 格式 API**:DeepSeek、Grok、Qwen、GPT、Ollama、vLLM 等 - **流式翻译显示**:翻译结果逐字实时显示 - **模型独立配置**:流式传输、结构化输出(JSON)、上下文历史、禁用思考 @@ -70,23 +70,23 @@ cd LiveTranslate 手动安装 ```bash -python -m venv .venv +uv venv --python 3.12 .venv .venv\Scripts\activate # PyTorch(三选一) -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu126 # CUDA -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 # CUDA(RTX 50 系列) -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # 仅 CPU +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu126 # CUDA +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 # CUDA(RTX 50 系列) +uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # 仅 CPU # 依赖 -pip install -r requirements.txt -pip install funasr --no-deps +uv sync --locked --inexact --no-install-package torch --no-install-package torchaudio +uv pip install funasr --no-deps # 启动 .venv\Scripts\python.exe main.py ``` -> FunASR 使用 `--no-deps` 安装,因为 `editdistance` 需要 C++ 编译器。`requirements.txt` 中已包含纯 Python 替代品 `editdistance-s`。 +> FunASR 使用 `--no-deps` 安装,因为 `editdistance` 需要 C++ 编译器。`pyproject.toml` 中已包含纯 Python 替代品 `editdistance-s`。 @@ -123,6 +123,7 @@ main.py 主入口,管线编排 ├── asr_sensevoice.py SenseVoice 后端 ├── asr_funasr_nano.py FunASR Nano 后端 ├── asr_anime_whisper.py Anime-Whisper 后端 (日语动画/Galgame) +├── asr_crispasr.py CrispASR ggml runtime 后端 ├── translator.py OpenAI 兼容翻译客户端 (流式/JSON/上下文) ├── model_manager.py 模型下载与缓存管理 ├── subtitle_overlay.py PyQt6 透明悬浮窗 @@ -136,6 +137,7 @@ main.py 主入口,管线编排 - [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — 基于 CTranslate2 的 Whisper 推理 - [FunASR](https://github.com/modelscope/FunASR) — SenseVoice / Fun-ASR-Nano - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — 日语动画/Galgame 专用 ASR +- CrispASR — ggml C++ ASR runtime hub,使用 GGUF/bin 单文件模型,LiveTranslate 通过 ASR worker 内的 Python binding 调用 - [Silero VAD](https://github.com/snakers4/silero-vad) — 语音活动检测 ## Star History diff --git a/asr_crispasr.py b/asr_crispasr.py new file mode 100644 index 0000000..18f17c0 --- /dev/null +++ b/asr_crispasr.py @@ -0,0 +1,332 @@ +import inspect +import logging +import os +from pathlib import Path +from typing import Any + +import numpy as np + +log = logging.getLogger("LiveTranslate.CrispASR") + + +def _prepare_crispasr_dll_path(): + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + try: + import crispasr + except Exception: + return + package_dir = Path(crispasr.__file__).resolve().parent + for path in (package_dir, package_dir / "bin"): + if path.is_dir(): + os.add_dll_directory(str(path)) + + +class CrispASREngine: + """CrispASR Python binding adapter. + + The binding is intentionally imported only inside the worker process. The + adapter accepts LiveTranslate's VAD-segmented 16 kHz mono float32 ndarray + and normalizes CrispASR result variants into the existing ASR result dict. + """ + + def __init__( + self, + model_path: str, + backend: str = "auto", + gpu_backend: str = "auto", + device_index: int = 0, + language: str = "auto", + punc_model: str | None = "auto", + n_threads: int | None = None, + unified_memory: bool = True, + ): + self.model_path = str(Path(model_path).resolve()) + self.backend = backend or "auto" + self._session_backend = None if self.backend in ("", "auto") else self.backend + self.gpu_backend = gpu_backend or "auto" + self.device_index = int(device_index or 0) + self.language = language if language and language != "auto" else None + self.punc_model = None if punc_model in (None, "", "off", "none") else punc_model + self._session = None + self._punc = None + + if not Path(self.model_path).is_file(): + raise FileNotFoundError(f"CrispASR model file not found: {self.model_path}") + + os.environ.setdefault("CRISPASR_ARG_DEVICE", str(self.device_index)) + if unified_memory: + os.environ.setdefault("GGML_CUDA_ENABLE_UNIFIED_MEMORY", "1") + + import crispasr + _prepare_crispasr_dll_path() + + session_cls = getattr(crispasr, "Session", None) + if session_cls is None: + raise RuntimeError("crispasr.Session is not available") + + self._session = self._open_session(session_cls, n_threads=n_threads) + self._punc = self._open_punctuation_model(crispasr) + log.info( + "CrispASR loaded: " + f"model={self.model_path}, backend={self.backend}, " + f"gpu_backend={self.gpu_backend}, device={self.device_index}" + ) + + def _open_session(self, session_cls, n_threads: int | None): + kwargs = { + "model_path": self.model_path, + "backend": self._session_backend, + "gpu_backend": self.gpu_backend, + "device_index": self.device_index, + "language": self.language, + "punc_model": self.punc_model, + "n_threads": n_threads, + } + aliases = { + "model": self.model_path, + "path": self.model_path, + "model_file": self.model_path, + "device": self.device_index, + "threads": n_threads, + } + + try: + params = inspect.signature(session_cls).parameters + except (TypeError, ValueError): + params = {} + + attempts = [] + if params: + accepted = { + key: value + for key, value in kwargs.items() + if key in params and value is not None + } + accepted.update( + { + key: value + for key, value in aliases.items() + if key in params and value is not None + } + ) + if "model_path" not in accepted and "model" not in accepted and "path" not in accepted: + attempts.append((self.model_path, accepted)) + else: + attempts.append((None, accepted)) + + explicit = { + key: value for key, value in kwargs.items() if value is not None + } + attempts.extend( + [ + (self.model_path, {k: v for k, v in explicit.items() if k != "model_path"}), + (self.model_path, {}), + (None, {"model_path": self.model_path}), + ] + ) + + last_error = None + for arg0, attempt_kwargs in attempts: + try: + if arg0 is None: + return session_cls(**attempt_kwargs) + return session_cls(arg0, **attempt_kwargs) + except TypeError as exc: + last_error = exc + continue + raise RuntimeError(f"Failed to create crispasr.Session: {last_error}") + + def _open_punctuation_model(self, crispasr): + if not self.punc_model: + return None + punc_cls = getattr(crispasr, "PuncModel", None) or getattr( + crispasr, "PunctuationModel", None + ) + if punc_cls is None: + return None + try: + return punc_cls(self.punc_model) + except Exception as exc: + log.warning(f"CrispASR punctuation model unavailable: {exc}") + return None + + def set_language(self, language: str): + self.language = language if language and language != "auto" else None + for name in ("set_language", "set_lang"): + fn = getattr(self._session, name, None) + if fn: + fn(self.language or "auto") + return + + def unload(self): + session = self._session + self._session = None + if session is None: + return + for name in ("close", "shutdown", "free", "reset"): + fn = getattr(session, name, None) + if fn: + try: + fn() + break + except Exception: + log.warning(f"CrispASR session {name} failed", exc_info=True) + + def transcribe(self, audio: np.ndarray) -> dict | None: + if audio.size == 0: + return None + if audio.dtype != np.float32 or not audio.flags["C_CONTIGUOUS"]: + audio = np.ascontiguousarray(audio, dtype=np.float32) + + result = self._call_transcribe(audio) + normalized = self._normalize_result(result) + if normalized and self._punc is not None: + normalized["text"] = self._apply_punctuation(normalized["text"]) + return normalized + + def _call_transcribe(self, audio: np.ndarray): + for name in ("transcribe", "recognize", "infer"): + fn = getattr(self._session, name, None) + if not fn: + continue + try: + params = inspect.signature(fn).parameters + except (TypeError, ValueError): + params = {} + kwargs = {} + if "language" in params and self.language: + kwargs["language"] = self.language + if "sample_rate" in params: + kwargs["sample_rate"] = 16000 + elif "sampling_rate" in params: + kwargs["sampling_rate"] = 16000 + return fn(audio, **kwargs) + raise RuntimeError("CrispASR session has no transcribe/recognize/infer method") + + def _apply_punctuation(self, text: str) -> str: + if not text: + return text + for name in ("restore", "punctuate", "process", "apply"): + fn = getattr(self._punc, name, None) + if not fn: + continue + try: + value = fn(text) + return str(value).strip() or text + except Exception as exc: + log.warning(f"CrispASR punctuation failed: {exc}") + return text + return text + + def _normalize_result(self, result: Any) -> dict | None: + if result is None: + return None + if isinstance(result, str): + text = result.strip() + return self._result(text) if text else None + + data = {"segments": result} if isinstance(result, (list, tuple)) else self._to_dict(result) + text = self._extract_text(data).strip() + if not text: + return None + + language = ( + data.get("language") + or data.get("language_detected") + or data.get("lang") + or self.language + or "auto" + ) + normalized = self._result(text, str(language)) + words = self._extract_words(data) + if words: + normalized["words"] = words + return normalized + + def _result(self, text: str, language: str = "auto") -> dict: + return { + "text": text, + "language": language, + "language_name": language, + } + + def _to_dict(self, value: Any) -> dict: + if isinstance(value, dict): + return value + if hasattr(value, "to_dict"): + try: + return value.to_dict() + except Exception: + pass + if hasattr(value, "_asdict"): + try: + return value._asdict() + except Exception: + pass + data = {} + for name in ( + "text", + "transcript", + "segments", + "language", + "language_detected", + "lang", + "words", + "word", + "start", + "end", + "confidence", + "probability", + ): + if hasattr(value, name): + data[name] = getattr(value, name) + return data + + def _extract_text(self, data: dict) -> str: + text = data.get("text") or data.get("transcript") + if text: + return str(text) + segments = data.get("segments") or data.get("result") or [] + if isinstance(segments, dict): + segments = segments.values() + parts = [] + for segment in segments: + if isinstance(segment, str): + parts.append(segment) + continue + seg_data = self._to_dict(segment) + seg_text = seg_data.get("text") or seg_data.get("transcript") + if seg_text: + parts.append(str(seg_text)) + return " ".join(part.strip() for part in parts if part and part.strip()) + + def _extract_words(self, data: dict) -> list[dict]: + raw_words = data.get("words") or [] + segments = data.get("segments") or data.get("result") or [] + if not raw_words and segments: + if isinstance(segments, dict): + segments = segments.values() + for segment in segments: + seg_data = self._to_dict(segment) + raw_words.extend(seg_data.get("words") or []) + + words = [] + for word in raw_words: + word_data = self._to_dict(word) + text = word_data.get("word") or word_data.get("text") + if not text: + continue + words.append( + { + "word": str(text), + "start": float(word_data.get("start") or 0.0), + "end": float(word_data.get("end") or 0.0), + "probability": float( + word_data.get("probability") + or word_data.get("confidence") + or 1.0 + ), + } + ) + return words diff --git a/asr_worker.py b/asr_worker.py index 368f9a8..c06f5d7 100644 --- a/asr_worker.py +++ b/asr_worker.py @@ -78,6 +78,30 @@ def _load_engine(config: dict): worker_device = parsed_device if parsed_device == "cpu" else f"cuda:{device_index}" engine = AnimeWhisperEngine(device=worker_device, hub=hub) + elif engine_type == "crispasr": + from asr_crispasr import CrispASREngine + + gpu_backend = config.get("crispasr_gpu_backend", "auto") + if parsed_device == "cpu": + gpu_backend = "cpu" + elif gpu_backend == "auto" and parsed_device.startswith("cuda"): + gpu_backend = "cuda" + device_index = int(config.get("crispasr_device_index", device_index)) + os_env_device = str(device_index) + import os + + os.environ["CRISPASR_ARG_DEVICE"] = os_env_device + if config.get("crispasr_unified_memory", True): + os.environ["GGML_CUDA_ENABLE_UNIFIED_MEMORY"] = "1" + engine = CrispASREngine( + model_path=config["crispasr_model_path"], + backend=config.get("crispasr_backend", "auto"), + gpu_backend=gpu_backend, + device_index=device_index, + language=language, + punc_model=config.get("crispasr_punc_model", "auto"), + unified_memory=config.get("crispasr_unified_memory", True), + ) else: from asr_engine import ASREngine diff --git a/build_release.ps1 b/build_release.ps1 index 1230973..dc5665c 100644 --- a/build_release.ps1 +++ b/build_release.ps1 @@ -85,6 +85,7 @@ $Root = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $Root $Uv = Join-Path $Root "tools\uv.exe" $env:UV_LINK_MODE = "copy" +$CrispAsrVersion = "v0.7.2" function Enable-SystemProxy { # uv (Python download) and pip honor *_PROXY env vars but not the Windows @@ -121,6 +122,60 @@ function Enable-SystemProxy { } Enable-SystemProxy +function Install-CrispAsrNativeRuntime { + param( + [string]$PythonExe, + [bool]$UseCuda + ) + + Write-Host "Installing CrispASR native runtime..." -ForegroundColor Cyan + try { + $target = & $PythonExe -c "import crispasr, pathlib; print(pathlib.Path(crispasr.__file__).resolve().parent)" + if ($LASTEXITCODE -ne 0 -or -not $target) { + throw "Could not locate the installed crispasr package" + } + $target = $target.Trim() + if (-not (Test-Path $target)) { + throw "Python package 'crispasr' is not installed" + } + + $variant = if ($UseCuda) { "cuda" } else { "cpu" } + $asset = if ($UseCuda) { + "libcrispasr-windows-x86_64-cuda.tar.gz" + } else { + "libcrispasr-windows-x86_64.tar.gz" + } + $url = "https://github.com/CrispStrobe/CrispASR/releases/download/$CrispAsrVersion/$asset" + $tmpDir = Join-Path $env:TEMP "livetranslate-crispasr-$variant" + $archive = Join-Path $tmpDir $asset + if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir } + New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + + Write-Host "Downloading $asset" -ForegroundColor Gray + Invoke-WebRequest -Uri $url -OutFile $archive + & tar -xzf $archive -C $tmpDir + if ($LASTEXITCODE -ne 0) { throw "Failed to extract $asset" } + + $runtimeRoot = Get-ChildItem -Path $tmpDir -Directory | Select-Object -First 1 + if (-not $runtimeRoot) { throw "Extracted CrispASR runtime directory not found" } + $bin = Join-Path $runtimeRoot.FullName "bin" + if (-not (Test-Path (Join-Path $bin "crispasr.dll"))) { + throw "crispasr.dll not found in $asset" + } + + Copy-Item -Path (Join-Path $bin "*.dll") -Destination $target -Force + Write-Host "CrispASR native runtime installed ($variant)" -ForegroundColor Green + } catch { + if ($UseCuda) { + Write-Host "CUDA CrispASR runtime failed: $($_.Exception.Message)" -ForegroundColor Yellow + Install-CrispAsrNativeRuntime -PythonExe $PythonExe -UseCuda $false + } else { + Write-Host "CrispASR native runtime installation failed: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "CrispASR will not run until libcrispasr/crispasr.dll is installed" -ForegroundColor Yellow + } + } +} + Write-Host "Creating virtual environment with Python 3.12..." -ForegroundColor Cyan & $Uv venv --python 3.12 --managed-python .venv if ($LASTEXITCODE -ne 0) { Write-Host "Failed to create venv" -ForegroundColor Red; exit 1 } @@ -144,11 +199,16 @@ Write-Host "Installing PyTorch (this may take a while)..." -ForegroundColor Cyan if ($LASTEXITCODE -ne 0) { Write-Host "PyTorch install failed" -ForegroundColor Red; exit 1 } Write-Host "Installing dependencies..." -ForegroundColor Cyan -& $Uv pip install --python $Py -r requirements.txt +& $Uv sync --python $Py --locked --inexact --no-install-package torch --no-install-package torchaudio if ($LASTEXITCODE -ne 0) { Write-Host "Dependency install failed" -ForegroundColor Red; exit 1 } +if (Test-Path (Join-Path $Root "repair_torch_metadata.ps1")) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $Root "repair_torch_metadata.ps1") -PythonExe $Py +} + +Install-CrispAsrNativeRuntime -PythonExe $Py -UseCuda ($Index -notlike "*cpu*") + & $Uv pip install --python $Py funasr --no-deps -& $Uv pip install --python $Py pysbd Write-Host "Setup complete." -ForegroundColor Green '@ diff --git a/config.yaml b/config.yaml index b1ca405..2a6cb98 100644 --- a/config.yaml +++ b/config.yaml @@ -9,12 +9,19 @@ audio: chunk_duration: 0.032 asr: - # ASR engine: whisper, funasr, anime-whisper + # ASR engine: whisper, funasr, anime-whisper, crispasr asr_engine: "funasr" # Model size: tiny, base, small, medium, large-v3 model_size: "medium" # FunASR model: sensevoice-small, funasr-nano-2512, funasr-mlt-nano-2512 funasr_model: "sensevoice-small" + # CrispASR GGUF model key or local .gguf/.bin path + crispasr_model: "parakeet-tdt-0.6b-v3-q4_k" + crispasr_backend: "auto" + crispasr_gpu_backend: "auto" + crispasr_device_index: 0 + crispasr_punc_model: "auto" + crispasr_unified_memory: true # Device: cuda or cpu device: "cuda" # Compute type: float16, int8_float16, int8 diff --git a/control_panel.py b/control_panel.py index a867a2b..a821602 100644 --- a/control_panel.py +++ b/control_panel.py @@ -8,6 +8,7 @@ from PyQt6.QtGui import QFont from PyQt6.QtWidgets import ( QApplication, + QCheckBox, QColorDialog, QComboBox, QDoubleSpinBox, @@ -33,17 +34,22 @@ ModelEditDialog, ) from model_manager import ( + DEFAULT_CRISPASR_MODEL, DEFAULT_FUNASR_MODEL, MODELS_DIR, _WHISPER_SIZES, + crispasr_model_options, dir_size, funasr_model_options, funasr_supports_padding, format_size, get_cache_entries, + list_local_crispasr_models, list_local_faster_whisper_models, migrate_funasr_settings, + normalize_crispasr_model_key, normalize_funasr_model_key, + resolve_custom_crispasr_model, resolve_custom_whisper_model, ) from i18n import t, LANGUAGES @@ -114,6 +120,22 @@ def __init__(self, config, saved_settings=None): "funasr_model": config["asr"].get( "funasr_model", DEFAULT_FUNASR_MODEL ), + "crispasr_model": config["asr"].get( + "crispasr_model", DEFAULT_CRISPASR_MODEL + ), + "crispasr_backend": config["asr"].get("crispasr_backend", "auto"), + "crispasr_gpu_backend": config["asr"].get( + "crispasr_gpu_backend", "auto" + ), + "crispasr_device_index": config["asr"].get( + "crispasr_device_index", 0 + ), + "crispasr_punc_model": config["asr"].get( + "crispasr_punc_model", "auto" + ), + "crispasr_unified_memory": config["asr"].get( + "crispasr_unified_memory", True + ), "asr_device": "cuda", "sensevoice_pad_seconds": config["asr"].get( "sensevoice_pad_seconds", 0.5 @@ -160,6 +182,32 @@ def __init__(self, config, saved_settings=None): "whisper_pad_seconds", config["asr"].get("whisper_pad_seconds", 0.5), ) + self._current_settings.setdefault( + "crispasr_model", + config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL), + ) + crispasr_setting = self._current_settings.get("crispasr_model") + if not crispasr_setting: + self._current_settings["crispasr_model"] = DEFAULT_CRISPASR_MODEL + self._current_settings.setdefault( + "crispasr_backend", config["asr"].get("crispasr_backend", "auto") + ) + self._current_settings.setdefault( + "crispasr_gpu_backend", + config["asr"].get("crispasr_gpu_backend", "auto"), + ) + self._current_settings.setdefault( + "crispasr_device_index", + config["asr"].get("crispasr_device_index", 0), + ) + self._current_settings.setdefault( + "crispasr_punc_model", + config["asr"].get("crispasr_punc_model", "auto"), + ) + self._current_settings.setdefault( + "crispasr_unified_memory", + config["asr"].get("crispasr_unified_memory", True), + ) layout = QVBoxLayout(self) tabs = QTabWidget() @@ -205,12 +253,14 @@ def _create_vad_tab(self): f"[{t('asr_accurate')}] Whisper (faster-whisper)", f"[{t('asr_fast')}] FunASR", "Anime-Whisper (ja, anime/galgame)", + "CrispASR (ggml)", ] ) engine_map_idx = { "whisper": 0, "funasr": 1, "anime-whisper": 2, + "crispasr": 3, } engine_idx = engine_map_idx.get(s.get("asr_engine"), 0) self._asr_engine.setCurrentIndex(engine_idx) @@ -268,6 +318,68 @@ def _create_vad_tab(self): asr_layout.addWidget(self._funasr_model_label, 3, 0) asr_layout.addWidget(self._funasr_model_combo, 3, 1) + self._crispasr_model_label = QLabel(t("label_crispasr_model")) + self._crispasr_model_combo = QComboBox() + saved_crispasr_model = s.get("crispasr_model", DEFAULT_CRISPASR_MODEL) + self._populate_crispasr_models(saved_crispasr_model) + self._crispasr_model_combo.currentIndexChanged.connect( + self._on_crispasr_setting_changed + ) + asr_layout.addWidget(self._crispasr_model_label, 4, 0) + asr_layout.addWidget(self._crispasr_model_combo, 4, 1) + + self._crispasr_gpu_label = QLabel(t("label_crispasr_gpu_backend")) + self._crispasr_gpu_backend = QComboBox() + self._crispasr_gpu_backend.addItems(["Auto", "CUDA", "Vulkan", "CPU"]) + gpu_backend = str(s.get("crispasr_gpu_backend", "auto")).lower() + gpu_idx = {"auto": 0, "cuda": 1, "vulkan": 2, "cpu": 3}.get(gpu_backend, 0) + self._crispasr_gpu_backend.setCurrentIndex(gpu_idx) + self._crispasr_gpu_backend.currentIndexChanged.connect( + self._on_crispasr_setting_changed + ) + asr_layout.addWidget(self._crispasr_gpu_label, 5, 0) + asr_layout.addWidget(self._crispasr_gpu_backend, 5, 1) + + self._crispasr_device_label = QLabel(t("label_crispasr_device_index")) + self._crispasr_device_index = QSpinBox() + self._crispasr_device_index.setRange(0, 16) + self._crispasr_device_index.setValue(int(s.get("crispasr_device_index", 0) or 0)) + self._crispasr_device_index.valueChanged.connect( + self._on_crispasr_setting_changed + ) + asr_layout.addWidget(self._crispasr_device_label, 6, 0) + asr_layout.addWidget(self._crispasr_device_index, 6, 1) + + self._crispasr_punc_label = QLabel(t("label_crispasr_punc_model")) + self._crispasr_punc_model = QComboBox() + for label, value in ( + ("Auto", "auto"), + ("Off", "off"), + ("FireRedPunc", "firered"), + ("fullstop", "fullstop"), + ("punctuate-all", "punctuate-all"), + ("PCS", "pcs"), + ): + self._crispasr_punc_model.addItem(label, value) + punc = str(s.get("crispasr_punc_model", "auto")).lower() + punc_idx = self._crispasr_punc_model.findData(punc) + if punc_idx >= 0: + self._crispasr_punc_model.setCurrentIndex(punc_idx) + self._crispasr_punc_model.currentIndexChanged.connect( + self._on_crispasr_setting_changed + ) + asr_layout.addWidget(self._crispasr_punc_label, 7, 0) + asr_layout.addWidget(self._crispasr_punc_model, 7, 1) + + self._crispasr_unified_memory = QCheckBox(t("label_crispasr_unified_memory")) + self._crispasr_unified_memory.setChecked( + bool(s.get("crispasr_unified_memory", True)) + ) + self._crispasr_unified_memory.toggled.connect( + self._on_crispasr_setting_changed + ) + asr_layout.addWidget(self._crispasr_unified_memory, 8, 1) + self._whisper_pad_label = QLabel(t("label_whisper_padding")) self._whisper_pad_seconds = QDoubleSpinBox() self._whisper_pad_seconds.setRange(0.0, 5.0) @@ -281,8 +393,8 @@ def _create_vad_tab(self): self._whisper_pad_seconds.setSuffix(" s") self._whisper_pad_seconds.setSpecialValueText(t("whisper_padding_off")) self._whisper_pad_seconds.setToolTip(t("whisper_padding_tooltip")) - asr_layout.addWidget(self._whisper_pad_label, 4, 0) - asr_layout.addWidget(self._whisper_pad_seconds, 4, 1) + asr_layout.addWidget(self._whisper_pad_label, 9, 0) + asr_layout.addWidget(self._whisper_pad_seconds, 9, 1) self._whisper_pad_seconds.valueChanged.connect(self._auto_save) self._sensevoice_pad_label = QLabel(t("label_sensevoice_padding")) @@ -298,8 +410,8 @@ def _create_vad_tab(self): self._sensevoice_pad_seconds.setSuffix(" s") self._sensevoice_pad_seconds.setSpecialValueText(t("sensevoice_padding_off")) self._sensevoice_pad_seconds.setToolTip(t("sensevoice_padding_tooltip")) - asr_layout.addWidget(self._sensevoice_pad_label, 5, 0) - asr_layout.addWidget(self._sensevoice_pad_seconds, 5, 1) + asr_layout.addWidget(self._sensevoice_pad_label, 10, 0) + asr_layout.addWidget(self._sensevoice_pad_seconds, 10, 1) self._sensevoice_pad_seconds.valueChanged.connect(self._auto_save) self._audio_device = QComboBox() @@ -321,8 +433,8 @@ def _create_vad_tab(self): self._audio_device.setCurrentIndex(idx) else: self._audio_device.setCurrentIndex(1) # system default - asr_layout.addWidget(QLabel(t("label_audio")), 6, 0) - asr_layout.addWidget(self._audio_device, 6, 1) + asr_layout.addWidget(QLabel(t("label_audio")), 11, 0) + asr_layout.addWidget(self._audio_device, 11, 1) self._audio_device.currentIndexChanged.connect(self._auto_save) self._mic_device = QComboBox() @@ -343,16 +455,16 @@ def _create_vad_tab(self): idx = self._mic_device.findText(saved_mic) if idx >= 0: self._mic_device.setCurrentIndex(idx) - asr_layout.addWidget(QLabel(t("label_mic")), 7, 0) - asr_layout.addWidget(self._mic_device, 7, 1) + asr_layout.addWidget(QLabel(t("label_mic")), 12, 0) + asr_layout.addWidget(self._mic_device, 12, 1) self._mic_device.currentIndexChanged.connect(self._auto_save) self._hub_combo = QComboBox() self._hub_combo.addItems([t("hub_modelscope"), t("hub_huggingface")]) saved_hub = s.get("hub", "ms") self._hub_combo.setCurrentIndex(0 if saved_hub == "ms" else 1) - asr_layout.addWidget(QLabel(t("label_hub")), 8, 0) - asr_layout.addWidget(self._hub_combo, 8, 1) + asr_layout.addWidget(QLabel(t("label_hub")), 13, 0) + asr_layout.addWidget(self._hub_combo, 13, 1) self._hub_combo.currentIndexChanged.connect(self._auto_save) self._ui_lang_combo = QComboBox() @@ -361,8 +473,8 @@ def _create_vad_tab(self): saved_lang = s.get("ui_lang", get_lang()) self._ui_lang_combo.setCurrentIndex(0 if saved_lang == "en" else 1) - asr_layout.addWidget(QLabel(t("label_ui_lang")), 9, 0) - asr_layout.addWidget(self._ui_lang_combo, 9, 1) + asr_layout.addWidget(QLabel(t("label_ui_lang")), 14, 0) + asr_layout.addWidget(self._ui_lang_combo, 14, 1) self._ui_lang_combo.currentIndexChanged.connect(self._on_ui_lang_changed) layout.addWidget(asr_group) @@ -387,11 +499,23 @@ def _create_vad_tab(self): whisper_layout.addWidget(self._whisper_dl_btn) layout.addWidget(self._whisper_group) self._whisper_group.setVisible(engine_idx == 0) + + self._crispasr_group = QGroupBox(t("group_download_crispasr")) + crispasr_layout = QHBoxLayout(self._crispasr_group) + self._crispasr_status = QLabel("") + self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") + crispasr_layout.addWidget(self._crispasr_status, 1) + self._crispasr_dl_btn = QPushButton(t("btn_download_crispasr")) + self._crispasr_dl_btn.clicked.connect(self._download_crispasr) + crispasr_layout.addWidget(self._crispasr_dl_btn) + layout.addWidget(self._crispasr_group) + self._crispasr_group.setVisible(engine_idx == 3) self._asr_engine.currentIndexChanged.connect( - self._on_engine_changed_whisper_vis + self._on_asr_engine_changed ) - self._on_engine_changed_whisper_vis(engine_idx) + self._on_asr_engine_changed(engine_idx) self._update_whisper_size_label() + self._update_crispasr_status() mode_group = QGroupBox(t("group_vad_mode")) mode_layout = QVBoxLayout(mode_group) @@ -478,8 +602,6 @@ def _create_vad_tab(self): timing_layout.addWidget(QLabel(t("label_silence_dur")), 3, 0) timing_layout.addWidget(self._silence_duration, 3, 1) - from PyQt6.QtWidgets import QCheckBox - self._incremental_asr_cb = QCheckBox(t("label_incremental_asr")) self._incremental_asr_cb.setToolTip(t("incremental_asr_tooltip")) self._incremental_asr_cb.setChecked(s.get("incremental_asr", False)) @@ -1077,7 +1199,11 @@ def _delete_all_and_exit(self): return for name, path, _ in self._cache_entries: try: - shutil.rmtree(path) + path_obj = Path(path) + if path_obj.is_file(): + path_obj.unlink() + else: + shutil.rmtree(path_obj) log.info(f"Deleted: {path}") except Exception as e: log.error(f"Failed to delete {path}: {e}") @@ -1087,12 +1213,25 @@ def _get_asr_lang_code(self) -> str: """Get the language code from the ASR language combo (stored as userData).""" return self._asr_lang.currentData() or "auto" - def _on_engine_changed_whisper_vis(self, index): + def _on_asr_engine_changed(self, index): self._whisper_group.setVisible(index == 0) is_funasr = index == 1 + is_crispasr = index == 3 + if hasattr(self, "_crispasr_group"): + self._crispasr_group.setVisible(is_crispasr) if hasattr(self, "_funasr_model_combo"): self._funasr_model_label.setVisible(is_funasr) self._funasr_model_combo.setVisible(is_funasr) + if hasattr(self, "_crispasr_model_combo"): + self._crispasr_model_label.setVisible(is_crispasr) + self._crispasr_model_combo.setVisible(is_crispasr) + self._crispasr_gpu_label.setVisible(is_crispasr) + self._crispasr_gpu_backend.setVisible(is_crispasr) + self._crispasr_device_label.setVisible(is_crispasr) + self._crispasr_device_index.setVisible(is_crispasr) + self._crispasr_punc_label.setVisible(is_crispasr) + self._crispasr_punc_model.setVisible(is_crispasr) + self._crispasr_unified_memory.setVisible(is_crispasr) if hasattr(self, "_whisper_pad_seconds"): is_whisper = index == 0 self._whisper_pad_label.setVisible(is_whisper) @@ -1116,13 +1255,79 @@ def _selected_funasr_model(self) -> str: def _on_funasr_model_changed(self): self._current_settings["funasr_model"] = self._selected_funasr_model() - self._on_engine_changed_whisper_vis(self._asr_engine.currentIndex()) + self._on_asr_engine_changed(self._asr_engine.currentIndex()) + self._auto_save() + + def _selected_crispasr_model(self) -> str: + value = self._crispasr_model_combo.currentData() + value = str(value) if value else "" + if value in dict(crispasr_model_options()): + return normalize_crispasr_model_key(value) + if value: + return value + return DEFAULT_CRISPASR_MODEL + + def _selected_crispasr_gpu_backend(self) -> str: + return ["auto", "cuda", "vulkan", "cpu"][self._crispasr_gpu_backend.currentIndex()] + + def _selected_crispasr_punc_model(self) -> str: + value = self._crispasr_punc_model.currentData() + return str(value) if value else "auto" + + def _on_crispasr_setting_changed(self): + self._current_settings["crispasr_model"] = self._selected_crispasr_model() + self._current_settings["crispasr_gpu_backend"] = ( + self._selected_crispasr_gpu_backend() + ) + self._current_settings["crispasr_device_index"] = ( + self._crispasr_device_index.value() + ) + self._current_settings["crispasr_punc_model"] = ( + self._selected_crispasr_punc_model() + ) + self._current_settings["crispasr_unified_memory"] = ( + self._crispasr_unified_memory.isChecked() + ) + self._update_crispasr_status() self._auto_save() def _selected_whisper_model(self) -> str: value = self._whisper_size_combo.currentData() return str(value) if value else self._whisper_size_combo.currentText() + def _populate_crispasr_models(self, saved_value: str): + self._crispasr_model_combo.clear() + builtin_models = dict(crispasr_model_options()) + for key, display_name in crispasr_model_options(): + self._crispasr_model_combo.addItem(display_name, key) + + local_prefix = t("crispasr_local_prefix") + for item in list_local_crispasr_models(): + idx = self._crispasr_model_combo.count() + self._crispasr_model_combo.addItem( + f"{local_prefix}: {item['name']}", item["path"] + ) + self._crispasr_model_combo.setItemData( + idx, item["path"], Qt.ItemDataRole.ToolTipRole + ) + + if saved_value in builtin_models: + selected = saved_value + else: + selected = resolve_custom_crispasr_model(saved_value) or saved_value + idx = self._crispasr_model_combo.findData(selected) + if idx < 0: + idx = self._crispasr_model_combo.findText(saved_value) + if idx < 0 and selected: + label = f"{t('crispasr_missing_local')}: {Path(str(selected)).name}" + idx = self._crispasr_model_combo.count() + self._crispasr_model_combo.addItem(label, selected) + self._crispasr_model_combo.setItemData( + idx, str(selected), Qt.ItemDataRole.ToolTipRole + ) + if idx >= 0: + self._crispasr_model_combo.setCurrentIndex(idx) + def _populate_whisper_models(self, saved_value: str): self._whisper_size_combo.clear() for size in _WHISPER_SIZES: @@ -1209,6 +1414,49 @@ def _download_whisper(self): # Switch to Whisper engine with the downloaded size self._auto_save() + def _update_crispasr_status(self): + from model_manager import is_asr_cached, crispasr_profile + + model_key = self._selected_crispasr_model() + hub = self._current_settings.get("hub", "ms") + cached = is_asr_cached("crispasr", model_key, hub) + if model_key not in dict(crispasr_model_options()): + if cached: + self._crispasr_status.setText(t("crispasr_local_ready")) + self._crispasr_status.setStyleSheet("color: #4a4; font-size: 11px;") + else: + self._crispasr_status.setText(t("crispasr_invalid_local")) + self._crispasr_status.setStyleSheet("color: #d66; font-size: 11px;") + self._crispasr_dl_btn.setEnabled(False) + return + if cached: + self._crispasr_status.setText(t("crispasr_already_cached")) + self._crispasr_status.setStyleSheet("color: #4a4; font-size: 11px;") + self._crispasr_dl_btn.setEnabled(False) + else: + est = crispasr_profile(model_key).get("estimated_bytes", 0) + self._crispasr_status.setText(f"~{format_size(est)}") + self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") + self._crispasr_dl_btn.setEnabled(True) + + def _download_crispasr(self): + from model_manager import is_asr_cached, get_missing_models + + model_key = self._selected_crispasr_model() + hub = self._current_settings.get("hub", "ms") + if is_asr_cached("crispasr", model_key, hub): + return + missing = get_missing_models("crispasr", model_key, hub) + missing = [m for m in missing if m["type"] != "silero-vad"] + if not missing: + return + from dialogs import ModelDownloadDialog + + dlg = ModelDownloadDialog(missing, hub=hub, parent=self) + if dlg.exec() == dlg.DialogCode.Accepted: + self._update_crispasr_status() + self._auto_save() + # ── Model Management ── def _refresh_model_list(self): @@ -1419,6 +1667,7 @@ def _apply_settings(self): 0: "whisper", 1: "funasr", 2: "anime-whisper", + 3: "crispasr", } self._current_settings["asr_engine"] = engine_map.get( self._asr_engine.currentIndex(), "whisper" @@ -1427,6 +1676,20 @@ def _apply_settings(self): self._current_settings["whisper_model_size"] = ( self._selected_whisper_model() ) + self._current_settings["crispasr_model"] = self._selected_crispasr_model() + self._current_settings["crispasr_backend"] = "auto" + self._current_settings["crispasr_gpu_backend"] = ( + self._selected_crispasr_gpu_backend() + ) + self._current_settings["crispasr_device_index"] = ( + self._crispasr_device_index.value() + ) + self._current_settings["crispasr_punc_model"] = ( + self._selected_crispasr_punc_model() + ) + self._current_settings["crispasr_unified_memory"] = ( + self._crispasr_unified_memory.isChecked() + ) dev_text = self._asr_device.currentText() self._current_settings["asr_device"] = dev_text.split(" (")[0] audio_idx = self._audio_device.currentIndex() diff --git a/dialogs.py b/dialogs.py index 735bd7a..a573a12 100644 --- a/dialogs.py +++ b/dialogs.py @@ -406,6 +406,14 @@ def _download_worker(self): hub=self._hub, proxy=self._proxy, ) + elif m["type"].startswith("crispasr:"): + model_key = m["type"].split(":", 1)[1] + download_asr( + "crispasr", + model_size=model_key, + hub=self._hub, + proxy=self._proxy, + ) elif m["type"].startswith("whisper-"): size = m["type"].replace("whisper-", "") download_asr( diff --git a/i18n/CHANGELOG_en.md b/i18n/CHANGELOG_en.md index 3e97b19..a78e798 100644 --- a/i18n/CHANGELOG_en.md +++ b/i18n/CHANGELOG_en.md @@ -1,5 +1,11 @@ # Changelog +## 2026-06-19 +- Fix CrispASR startup failure with `No module named 'crispasr'`: added the CrispASR Python binding to `pyproject.toml`, pinned to GitHub Releases `v0.7.2` +- Installer now downloads the prebuilt Windows `libcrispasr` DLL runtime from Releases and places it next to the installed `crispasr` package +- NVIDIA systems prefer the CUDA CrispASR runtime and automatically fall back to the CPU runtime if download or installation fails +- Portable first-run bootstrap now installs both the CrispASR binding and native DLLs, preventing release builds from missing `crispasr.dll` + ## 2026-05-10 - New "Export to file" menu: original / translation / combined formats, accessible from overlay right-click menu and tray menu - New "Transcript persistence" (enabled by default): each session creates 3 files under `transcripts/` (original / translation / combined), appended in real time per segment — no longer bounded by the 50-message overlay cap diff --git a/i18n/CHANGELOG_zh.md b/i18n/CHANGELOG_zh.md index 2ffb85a..4263395 100644 --- a/i18n/CHANGELOG_zh.md +++ b/i18n/CHANGELOG_zh.md @@ -1,5 +1,11 @@ # 更新日志 +## 2026-06-19 +- 修复 CrispASR 启动时报 `No module named 'crispasr'`: `pyproject.toml` 新增 CrispASR Python binding 依赖, 固定到 GitHub Releases `v0.7.2` +- 安装脚本新增 CrispASR 原生运行时安装: 从 Releases 下载预编译 `libcrispasr` Windows DLL, 放入已安装的 `crispasr` 包目录 +- NVIDIA 环境优先安装 CUDA 版 CrispASR runtime, 下载或安装失败时自动回退 CPU runtime +- Portable 首次启动 bootstrap 同步安装 CrispASR binding 与原生 DLL, 避免发布包缺少 `crispasr.dll` + ## 2026-05-10 - 新增「导出到文件」: 支持原文 / 译文 / 原文+译文 三种格式, 悬浮窗右键菜单与托盘菜单均可触发 - 新增「转录持久化」(默认开启): 每次会话自动在 `transcripts/` 下创建 3 份文件 (原文 / 译文 / 全部), 每段识别结果实时追加写入, 不再受悬浮窗 50 条上限丢失早期文本 diff --git a/i18n/en.yaml b/i18n/en.yaml index 4ef4051..4792246 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -81,6 +81,11 @@ label_language_hint: "Language Hint:" asr_lang_auto: "Auto Detect" label_device: "Device:" label_funasr_model: "FunASR model:" +label_crispasr_model: "CrispASR model:" +label_crispasr_gpu_backend: "CrispASR GPU:" +label_crispasr_device_index: "CrispASR device:" +label_crispasr_punc_model: "CrispASR punctuation:" +label_crispasr_unified_memory: "Enable unified memory" label_whisper_padding: "Whisper padding:" whisper_padding_off: "Off" whisper_padding_tooltip: "Pads faster-whisper input to this duration bucket. 0 disables padding." @@ -152,6 +157,13 @@ whisper_local_prefix: "Local" whisper_local_ready: "Local model" whisper_missing_local: "Missing" whisper_invalid_local: "Local model unavailable" +group_download_crispasr: "CrispASR Model" +btn_download_crispasr: "Download" +crispasr_already_cached: "Already cached" +crispasr_local_prefix: "Local" +crispasr_local_ready: "Local model" +crispasr_missing_local: "Missing" +crispasr_invalid_local: "Local model unavailable" btn_open_folder: "Open Models Folder" btn_delete_all_exit: "Delete All && Exit" scanning: "Scanning..." diff --git a/i18n/zh.yaml b/i18n/zh.yaml index f2dec2b..9b1fec0 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -81,6 +81,11 @@ label_language_hint: "语言提示:" asr_lang_auto: "自动检测" label_device: "设备:" label_funasr_model: "FunASR 模型:" +label_crispasr_model: "CrispASR 模型:" +label_crispasr_gpu_backend: "CrispASR GPU:" +label_crispasr_device_index: "CrispASR 设备:" +label_crispasr_punc_model: "CrispASR 标点:" +label_crispasr_unified_memory: "启用统一内存" label_whisper_padding: "Whisper 补零:" whisper_padding_off: "关闭" whisper_padding_tooltip: "将 faster-whisper 输入补齐到指定秒数档位。0 表示关闭。" @@ -152,6 +157,13 @@ whisper_local_prefix: "本地" whisper_local_ready: "本地模型" whisper_missing_local: "缺失" whisper_invalid_local: "本地模型不可用" +group_download_crispasr: "CrispASR 模型" +btn_download_crispasr: "下载" +crispasr_already_cached: "已缓存" +crispasr_local_prefix: "本地" +crispasr_local_ready: "本地模型" +crispasr_missing_local: "缺失" +crispasr_invalid_local: "本地模型不可用" btn_open_folder: "打开模型文件夹" btn_delete_all_exit: "删除全部并退出" scanning: "扫描中..." diff --git a/install.ps1 b/install.ps1 index 74eb4c7..c9919ae 100644 --- a/install.ps1 +++ b/install.ps1 @@ -10,6 +10,63 @@ function Write-Ok { param($msg) Write-Host " OK: $msg" -ForegroundColor Green function Write-Warn { param($msg) Write-Host " WARN: $msg" -ForegroundColor Yellow } function Write-Err { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red } +$CrispAsrVersion = "v0.7.2" +$Uv = "uv" + +function Install-CrispAsrNativeRuntime { + param( + [string]$PythonExe, + [bool]$UseCuda + ) + + Write-Step "Installing CrispASR native runtime..." + try { + $target = & $PythonExe -c "import crispasr, pathlib; print(pathlib.Path(crispasr.__file__).resolve().parent)" + if ($LASTEXITCODE -ne 0 -or -not $target) { + throw "Could not locate the installed crispasr package" + } + $target = $target.Trim() + if (-not (Test-Path $target)) { + throw "Python package 'crispasr' is not installed" + } + + $variant = if ($UseCuda) { "cuda" } else { "cpu" } + $asset = if ($UseCuda) { + "libcrispasr-windows-x86_64-cuda.tar.gz" + } else { + "libcrispasr-windows-x86_64.tar.gz" + } + $url = "https://github.com/CrispStrobe/CrispASR/releases/download/$CrispAsrVersion/$asset" + $tmpDir = Join-Path $env:TEMP "livetranslate-crispasr-$variant" + $archive = Join-Path $tmpDir $asset + if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir } + New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + + Write-Host " Downloading $asset" -ForegroundColor Gray + Invoke-WebRequest -Uri $url -OutFile $archive + & tar -xzf $archive -C $tmpDir + if ($LASTEXITCODE -ne 0) { throw "Failed to extract $asset" } + + $root = Get-ChildItem -Path $tmpDir -Directory | Select-Object -First 1 + if (-not $root) { throw "Extracted CrispASR runtime directory not found" } + $bin = Join-Path $root.FullName "bin" + if (-not (Test-Path (Join-Path $bin "crispasr.dll"))) { + throw "crispasr.dll not found in $asset" + } + + Copy-Item -Path (Join-Path $bin "*.dll") -Destination $target -Force + Write-Ok "CrispASR native runtime installed ($variant)" + } catch { + if ($UseCuda) { + Write-Warn "CUDA CrispASR runtime failed: $($_.Exception.Message)" + Install-CrispAsrNativeRuntime -PythonExe $PythonExe -UseCuda $false + } else { + Write-Warn "CrispASR native runtime installation failed: $($_.Exception.Message)" + Write-Warn "CrispASR will not run until libcrispasr/crispasr.dll is installed" + } + } +} + function Enable-SystemProxy { # uv (Python download) and pip honor *_PROXY env vars but not the Windows # registry system proxy; bridge it here. An already-set env proxy wins. @@ -51,6 +108,52 @@ Write-Host "========================================" -ForegroundColor Magenta Enable-SystemProxy +# ── Step 0: Find uv ── +Write-Step "Detecting uv..." +try { + $uvVersion = & $Uv --version 2>&1 + if ($LASTEXITCODE -ne 0) { throw $uvVersion } + Write-Ok $uvVersion +} catch { + Write-Warn "uv not found" + $hasWinget = $false + try { + $null = & winget --version 2>&1 + if ($LASTEXITCODE -eq 0) { $hasWinget = $true } + } catch {} + + if ($hasWinget) { + Write-Host "" + Write-Host " uv can be installed automatically via winget." -ForegroundColor White + $answer = Read-Host " Install uv now? [Y/n]" + if ($answer -eq "" -or $answer -match "^[Yy]") { + Write-Step "Installing uv via winget..." + & winget install Astral.UV --accept-package-agreements --accept-source-agreements + if ($LASTEXITCODE -ne 0) { + Write-Err "winget install failed" + Read-Host "Press Enter to exit" + exit 1 + } + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + $uvVersion = & $Uv --version 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Err "uv installed but not found in PATH. Please close this window, reopen, and run install.bat again." + Read-Host "Press Enter to exit" + exit 1 + } + Write-Ok $uvVersion + } else { + Write-Err "uv is required. Install it from https://docs.astral.sh/uv/getting-started/installation/ and run install.bat again." + Read-Host "Press Enter to exit" + exit 1 + } + } else { + Write-Err "uv is required and winget is not available. Install uv from https://docs.astral.sh/uv/getting-started/installation/" + Read-Host "Press Enter to exit" + exit 1 + } +} + # ── Step 1: Find Python ── Write-Step "Detecting Python..." @@ -67,6 +170,19 @@ function Find-Python { } } catch {} } + # uv-managed Python is project-local and should count as available when it + # has already been downloaded. Do not download during detection. + foreach ($v in @("3.12", "3.11", "3.10")) { + try { + $exe = & $Uv python find $v --managed-python --no-python-downloads 2>&1 + if ($LASTEXITCODE -eq 0 -and $exe -and (Test-Path $exe.Trim())) { + $exe = $exe.Trim() + $ver = & $exe --version 2>&1 + Write-Ok "Found uv-managed $ver ($exe)" + return $exe + } + } catch {} + } # Fall back to plain commands, rejecting unsupported versions. foreach ($cmd in @("python", "python3", "py")) { try { @@ -162,7 +278,7 @@ if (Test-Path ".venv") { } else { Write-Warn "Existing venv is broken or incomplete, recreating..." Remove-Item -Recurse -Force .venv -ErrorAction SilentlyContinue - & $PythonCmd -m venv .venv + & $Uv venv --python $PythonCmd .venv if ($LASTEXITCODE -ne 0) { Write-Err "Failed to create venv" Read-Host "Press Enter to exit" @@ -171,7 +287,7 @@ if (Test-Path ".venv") { Write-Ok "Created .venv" } } else { - & $PythonCmd -m venv .venv + & $Uv venv --python $PythonCmd .venv if ($LASTEXITCODE -ne 0) { Write-Err "Failed to create venv" Read-Host "Press Enter to exit" @@ -180,18 +296,8 @@ if (Test-Path ".venv") { Write-Ok "Created .venv" } -$Pip = ".venv\Scripts\pip.exe" $Python = ".venv\Scripts\python.exe" -# Upgrade pip first -Write-Step "Upgrading pip..." -& $Python -m pip install --upgrade pip --quiet -if ($LASTEXITCODE -ne 0) { - Write-Warn "pip upgrade failed (non-critical, continuing with current pip)" -} else { - Write-Ok "pip upgraded" -} - # ── Step 3: Detect GPU ── Write-Step "Detecting GPU..." @@ -242,9 +348,9 @@ Write-Step "Installing PyTorch (this may take a few minutes)..." if ($HasNvidia) { Write-Host " Using index: $CudaVer" -ForegroundColor Gray - & $Pip install torch torchaudio --index-url https://download.pytorch.org/whl/$CudaVer + & $Uv pip install --python $Python torch torchaudio --index-url https://download.pytorch.org/whl/$CudaVer } else { - & $Pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu + & $Uv pip install --python $Python torch torchaudio --index-url https://download.pytorch.org/whl/cpu } if ($LASTEXITCODE -ne 0) { @@ -255,36 +361,36 @@ if ($LASTEXITCODE -ne 0) { Write-Ok "PyTorch installed" # ── Step 5: Install dependencies ── -Write-Step "Installing dependencies from requirements.txt..." +Write-Step "Syncing dependencies with uv..." -& $Pip install -r requirements.txt +& $Uv sync --python $Python --locked --inexact --no-install-package torch --no-install-package torchaudio if ($LASTEXITCODE -ne 0) { - Write-Err "Failed to install dependencies" + Write-Err "Failed to sync dependencies" Read-Host "Press Enter to exit" exit 1 } -Write-Ok "Dependencies installed" +Write-Ok "Dependencies synced" -# ── Step 6: Install FunASR (no-deps) ── +# uv sync intentionally skips torch/torchaudio because the correct wheel index +# depends on GPU support. Clean up stale torch metadata if a previous sync/install +# left duplicate dist-info directories behind. +if (Test-Path ".\repair_torch_metadata.ps1") { + & powershell -NoProfile -ExecutionPolicy Bypass -File ".\repair_torch_metadata.ps1" -PythonExe $Python +} + +# ── Step 6: Install CrispASR native runtime ── +Install-CrispAsrNativeRuntime -PythonExe $Python -UseCuda $HasNvidia + +# ── Step 7: Install FunASR (no-deps) ── Write-Step "Installing FunASR (--no-deps)..." -& $Pip install funasr --no-deps +& $Uv pip install --python $Python funasr --no-deps if ($LASTEXITCODE -ne 0) { Write-Warn "FunASR installation failed (non-critical, SenseVoice engine may not work)" } else { Write-Ok "FunASR installed" } -# ── Step 7: Install pysbd for incremental ASR ── -Write-Step "Installing pysbd..." - -& $Pip install pysbd -if ($LASTEXITCODE -ne 0) { - Write-Warn "pysbd installation failed (incremental ASR may not work)" -} else { - Write-Ok "pysbd installed" -} - # ── Done ── Write-Host "" Write-Host "========================================" -ForegroundColor Green diff --git a/main.py b/main.py index 6712390..cec45f9 100644 --- a/main.py +++ b/main.py @@ -18,17 +18,23 @@ from model_manager import ( DEFAULT_FUNASR_MODEL, + DEFAULT_CRISPASR_MODEL, apply_cache_env, + crispasr_profile, funasr_display_name, funasr_supports_padding, get_missing_models, + get_local_model_path, is_asr_cached, ASR_DISPLAY_NAMES, MODELS_DIR, + local_crispasr_display_name, local_faster_whisper_display_name, migrate_funasr_settings, + normalize_crispasr_model_key, normalize_asr_engine_selection, normalize_funasr_model_key, + resolve_custom_crispasr_model, resolve_custom_whisper_model, ) @@ -182,6 +188,9 @@ def __init__(self, config): self._funasr_model_key = normalize_funasr_model_key( config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL) ) + self._crispasr_model_key = normalize_crispasr_model_key( + config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL) + ) self._asr_lock = threading.RLock() self._vad_lock = threading.Lock() self._target_language = config["translation"]["target_language"] @@ -273,6 +282,12 @@ def _on_settings_changed(self, settings): "asr_device", "whisper_model_size", "funasr_model", + "crispasr_model", + "crispasr_backend", + "crispasr_gpu_backend", + "crispasr_device_index", + "crispasr_punc_model", + "crispasr_unified_memory", "hub", ) ): @@ -444,21 +459,73 @@ def _switch_asr_engine(self, engine_type: str): model_size = settings.get("whisper_model_size", model_size) model_path = None cache_model_key = model_size + crispasr_model = settings.get( + "crispasr_model", + self._config["asr"].get("crispasr_model", self._crispasr_model_key), + ) + crispasr_model_path_value = None + crispasr_backend = settings.get( + "crispasr_backend", self._config["asr"].get("crispasr_backend", "auto") + ) + crispasr_gpu_backend = settings.get( + "crispasr_gpu_backend", + self._config["asr"].get("crispasr_gpu_backend", "auto"), + ) + crispasr_device_index = int( + settings.get( + "crispasr_device_index", + self._config["asr"].get("crispasr_device_index", 0), + ) + or 0 + ) + crispasr_punc_model = settings.get( + "crispasr_punc_model", + self._config["asr"].get("crispasr_punc_model", "auto"), + ) + crispasr_unified_memory = bool( + settings.get( + "crispasr_unified_memory", + self._config["asr"].get("crispasr_unified_memory", True), + ) + ) if engine_type == "whisper": model_path = resolve_custom_whisper_model(model_size) if model_path: cache_model_key = model_path elif engine_type == "funasr": cache_model_key = funasr_model + elif engine_type == "crispasr": + custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) + if custom_crispasr_path: + cache_model_key = custom_crispasr_path + crispasr_model_path_value = custom_crispasr_path + else: + crispasr_model = normalize_crispasr_model_key(crispasr_model) + cache_model_key = crispasr_model + crispasr_model_path_value = get_local_model_path( + "crispasr", hub=hub, funasr_model=crispasr_model + ) compute = self._config["asr"]["compute_type"] if engine_type == "whisper": signature_model = cache_model_key elif engine_type == "funasr": signature_model = funasr_model + elif engine_type == "crispasr": + signature_model = ( + cache_model_key, + crispasr_backend, + crispasr_gpu_backend, + crispasr_device_index, + crispasr_punc_model, + crispasr_unified_memory, + ) else: signature_model = engine_type - signature = (engine_type, signature_model, device, hub, compute) + language = settings.get( + "asr_language", self._config["asr"].get("language", "auto") + ) + signature = (engine_type, signature_model, device, hub, compute, language) with self._asr_lock: current_asr = self._asr @@ -494,6 +561,27 @@ def _switch_asr_engine(self, engine_type: str): display_name = f"Whisper {display_model}" elif engine_type == "funasr": display_name = funasr_display_name(funasr_model) + elif engine_type == "crispasr": + custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) + if custom_crispasr_path: + display_model = ( + local_crispasr_display_name(crispasr_model) + or Path(crispasr_model).name + ) + display_name = f"CrispASR {display_model}" + else: + display_name = f"CrispASR {crispasr_profile(crispasr_model)['display_name']}" + + profile = ( + {} + if custom_crispasr_path + else crispasr_profile(crispasr_model) + ) + if ( + profile.get("needs_punctuation") + and crispasr_punc_model in (None, "", "auto") + ): + crispasr_punc_model = "auto" parent = ( self._panel if self._panel and self._panel.isVisible() else self._overlay @@ -506,9 +594,7 @@ def _switch_asr_engine(self, engine_type: str): "device": device, "compute_type": compute, "hub": hub, - "language": settings.get( - "asr_language", self._config["asr"].get("language", "auto") - ), + "language": language, "pad_seconds": ( settings.get( "sensevoice_pad_seconds", @@ -525,6 +611,18 @@ def _switch_asr_engine(self, engine_type: str): "download_root": str((MODELS_DIR / "huggingface" / "hub").resolve()), "display_name": display_name, } + if engine_type == "crispasr": + worker_config.update( + { + "crispasr_model": crispasr_model, + "crispasr_model_path": crispasr_model_path_value, + "crispasr_backend": crispasr_backend, + "crispasr_gpu_backend": crispasr_gpu_backend, + "crispasr_device_index": crispasr_device_index, + "crispasr_punc_model": crispasr_punc_model, + "crispasr_unified_memory": crispasr_unified_memory, + } + ) target_state = { "type": engine_type, "signature": signature, @@ -535,6 +633,9 @@ def _switch_asr_engine(self, engine_type: str): "whisper_model_size": model_size if engine_type == "whisper" else self._whisper_model_size, + "crispasr_model_key": crispasr_model + if engine_type == "crispasr" + else self._crispasr_model_key, "config": worker_config, "display_name": display_name, } @@ -553,6 +654,21 @@ def _switch_asr_engine(self, engine_type: str): self._asr is not None and self._asr.status == "ready" ) return + if engine_type == "crispasr": + crispasr_model_path_value = get_local_model_path( + "crispasr", hub=hub, funasr_model=cache_model_key + ) + if not crispasr_model_path_value: + QMessageBox.warning( + parent, + t("error_title"), + t("error_load_asr").format( + error="CrispASR model file was not found after download" + ), + ) + return + worker_config["crispasr_model_path"] = crispasr_model_path_value + target_state["config"] = worker_config with self._asr_lock: old_asr = self._asr @@ -563,6 +679,7 @@ def _switch_asr_engine(self, engine_type: str): "device": self._asr_device, "funasr_model_key": self._funasr_model_key, "whisper_model_size": self._whisper_model_size, + "crispasr_model_key": self._crispasr_model_key, "config": old_config, "display_name": (old_config or {}).get("display_name"), } @@ -629,6 +746,7 @@ def _activate_asr(client, state): self._asr_config = dict(state["config"]) if state["config"] else None self._funasr_model_key = state["funasr_model_key"] self._whisper_model_size = state["whisper_model_size"] + self._crispasr_model_key = state["crispasr_model_key"] self._asr_ready = True self._asr_error_count = 0 @@ -1519,13 +1637,21 @@ def main(): else: saved = saved or {} current_engine = saved.get("asr_engine", config["asr"].get("asr_engine", "funasr")) + current_engine, current_funasr_model = normalize_asr_engine_selection( + current_engine, saved.get("funasr_model", config["asr"].get("funasr_model")) + ) + if current_engine == "funasr": + startup_model_key = current_funasr_model + elif current_engine == "crispasr": + startup_model_key = saved.get( + "crispasr_model", + config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL), + ) + else: + startup_model_key = saved.get("whisper_model_size", config["asr"]["model_size"]) missing = get_missing_models( current_engine, - ( - saved.get("funasr_model", config["asr"].get("funasr_model")) - if current_engine == "funasr" - else saved.get("whisper_model_size", config["asr"]["model_size"]) - ), + startup_model_key, saved.get("hub", "ms"), ) if missing: diff --git a/model_manager.py b/model_manager.py index 0eae767..9759654 100644 --- a/model_manager.py +++ b/model_manager.py @@ -139,8 +139,27 @@ def asr_model_id( "funasr-mlt-nano": "Fun-ASR-MLT-Nano", "whisper": "Whisper", "anime-whisper": "Anime-Whisper", + "crispasr": "CrispASR", } +CRISPASR_MODEL_PROFILES = { + "parakeet-tdt-0.6b-v3-q4_k": { + "display_name": "Parakeet TDT 0.6B v3 Q4_K", + "repo_id": "cstr/parakeet-tdt-0.6b-v3-GGUF", + "filename": "parakeet-tdt-0.6b-v3-q4_k.gguf", + "backend": "auto", + "estimated_bytes": 650_000_000, + "languages": "25 EU", + "output_style": "native_punctuation", + "needs_punctuation": False, + "sha256": None, + }, +} + +DEFAULT_CRISPASR_MODEL = "parakeet-tdt-0.6b-v3-q4_k" +_CRISPASR_EXTS = {".gguf", ".bin"} +_CRISPASR_MIN_BYTES = 1_000_000 + _MODEL_SIZE_BYTES = { "silero-vad": 2_000_000, "sensevoice": 940_000_000, @@ -152,6 +171,7 @@ def asr_model_id( "whisper-medium": 1_530_000_000, "whisper-large-v3": 3_100_000_000, "anime-whisper": 3_100_000_000, + "crispasr:parakeet-tdt-0.6b-v3-q4_k": 650_000_000, } _WHISPER_SIZES = ["tiny", "base", "small", "medium", "large-v3"] @@ -220,6 +240,91 @@ def funasr_model_id(model_key: str | None, hub: str = "ms") -> str: return profile["huggingface_id"] if hub == "hf" else profile["modelscope_id"] +def normalize_crispasr_model_key(model_key: str | None) -> str: + if model_key in CRISPASR_MODEL_PROFILES: + return model_key + return DEFAULT_CRISPASR_MODEL + + +def crispasr_profile(model_key: str | None) -> dict: + return CRISPASR_MODEL_PROFILES[normalize_crispasr_model_key(model_key)] + + +def crispasr_model_options() -> list[tuple[str, str]]: + return [ + (key, profile["display_name"]) + for key, profile in CRISPASR_MODEL_PROFILES.items() + ] + + +def is_crispasr_model_file(path) -> bool: + if not path: + return False + path = Path(path) + try: + return ( + path.is_file() + and path.suffix.lower() in _CRISPASR_EXTS + and path.stat().st_size >= _CRISPASR_MIN_BYTES + ) + except OSError: + return False + + +def _custom_crispasr_path(value) -> Path | None: + if not value or value in CRISPASR_MODEL_PROFILES: + return None + path = Path(str(value)).expanduser() + if not path.is_absolute(): + path = APP_DIR / path + return path + + +def resolve_custom_crispasr_model(value) -> str | None: + path = _custom_crispasr_path(value) + if path and is_crispasr_model_file(path): + return str(path.resolve()) + return None + + +def _hf_repo_cache_dir(repo_id: str) -> Path: + org, name = repo_id.split("/", 1) + return MODELS_DIR / "huggingface" / "hub" / f"models--{org}--{name}" + + +def _hf_cached_file(repo_id: str, filename: str) -> Path | None: + snap_root = _hf_repo_cache_dir(repo_id) / "snapshots" + if not snap_root.exists(): + return None + try: + snapshots = sorted( + (p for p in snap_root.iterdir() if p.is_dir()), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) + except (OSError, PermissionError): + return None + for snap in snapshots: + path = snap / filename + if is_crispasr_model_file(path): + return path + return None + + +def _crispasr_legacy_model_path(model_key: str | None) -> Path: + profile = crispasr_profile(model_key) + return MODELS_DIR / "crispasr" / profile["filename"] + + +def crispasr_model_path(model_key: str | None, hub: str = "hf") -> str: + profile = crispasr_profile(model_key) + cached = _hf_cached_file(profile["repo_id"], profile["filename"]) + if cached: + return str(cached.resolve()) + legacy = _crispasr_legacy_model_path(model_key) + return str(legacy.resolve()) + + def _custom_whisper_path(value) -> Path | None: if not value or value in _WHISPER_SIZES: return None @@ -253,6 +358,21 @@ def _is_builtin_whisper_cache(path: Path) -> bool: return any(f"models--Systran--faster-whisper-{s}" in parts for s in _WHISPER_SIZES) +def _is_builtin_crispasr_cache(path: Path) -> bool: + parts = set(path.parts) + for profile in CRISPASR_MODEL_PROFILES.values(): + org, name = profile["repo_id"].split("/", 1) + if f"models--{org}--{name}" in parts: + return True + return False + + +def _is_hf_hub_cache(path: Path) -> bool: + parts = path.parts + marker = ("huggingface", "hub") + return any(parts[i : i + 2] == marker for i in range(len(parts) - 1)) + + def _hf_snapshot_name(path: Path) -> str | None: """Return 'org/repo' for .../models--org--repo/snapshots/.""" if path.parent.name != "snapshots": @@ -314,6 +434,61 @@ def local_faster_whisper_display_name(path) -> str | None: return item["name"] return _hf_snapshot_name(Path(resolved)) or Path(resolved).name + +def list_local_crispasr_models() -> list[dict]: + """Scan ./models for user-provided CrispASR single-file models.""" + if not MODELS_DIR.exists(): + return [] + + entries = [] + name_counts = {} + seen = set() + try: + files = [ + path + for ext in _CRISPASR_EXTS + for path in MODELS_DIR.rglob(f"*{ext}") + ] + except (OSError, PermissionError): + return [] + + for path in files: + if _is_builtin_crispasr_cache(path): + continue + if _is_hf_hub_cache(path): + continue + if path.name == "model.bin" and is_faster_whisper_model_dir(path.parent): + continue + if not is_crispasr_model_file(path): + continue + try: + resolved = str(path.resolve()) + except OSError: + continue + if resolved in seen: + continue + seen.add(resolved) + + name = path.name + name_counts[name] = name_counts.get(name, 0) + 1 + if name_counts[name] > 1: + name = f"{path.stem} ({path.parent.name}){path.suffix}" + entries.append({"name": name, "path": resolved}) + + entries.sort(key=lambda item: item["name"].lower()) + return entries + + +def local_crispasr_display_name(path) -> str | None: + resolved = resolve_custom_crispasr_model(path) + if not resolved: + return None + for item in list_local_crispasr_models(): + if item["path"] == resolved: + return item["name"] + return Path(resolved).name + + def apply_cache_env(): """Point all model caches to ./models/.""" resolved = str(MODELS_DIR.resolve()) @@ -379,6 +554,16 @@ def _hf_repo_complete(org: str, name: str, min_bytes: int = 50_000_000) -> bool: def is_asr_cached(engine_type, model_size="medium", hub="ms") -> bool: + if engine_type == "crispasr": + custom = resolve_custom_crispasr_model(model_size) + if custom: + return True + if model_size not in CRISPASR_MODEL_PROFILES: + return False + profile = crispasr_profile(model_size) + if _hf_cached_file(profile["repo_id"], profile["filename"]): + return True + return is_crispasr_model_file(_crispasr_legacy_model_path(model_size)) if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -440,6 +625,8 @@ def get_missing_models(engine, model_size, hub) -> list: if not is_asr_cached(engine, model_size, hub): if engine == "whisper" and model_size not in _WHISPER_SIZES: return missing + if engine == "crispasr" and model_size not in CRISPASR_MODEL_PROFILES: + return missing if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine] @@ -454,6 +641,11 @@ def get_missing_models(engine, model_size, hub) -> list: key = engine if engine != "whisper" else f"whisper-{model_size}" display = f"Whisper {model_size}" estimated_bytes = _MODEL_SIZE_BYTES.get(key, 0) + elif engine == "crispasr": + profile = crispasr_profile(model_size) + key = f"crispasr:{normalize_crispasr_model_key(model_size)}" + display = f"CrispASR {profile['display_name']}" + estimated_bytes = profile["estimated_bytes"] else: key = engine display = ASR_DISPLAY_NAMES.get(engine, engine) @@ -473,6 +665,20 @@ def get_local_model_path(engine_type, hub="ms", funasr_model: str | None = None) Checks the preferred hub first, then falls back to the other hub. """ + if engine_type == "crispasr": + model_value = funasr_model + custom = resolve_custom_crispasr_model(model_value) + if custom: + return custom + if model_value not in CRISPASR_MODEL_PROFILES: + return None + profile = crispasr_profile(model_value) + path = _hf_cached_file(profile["repo_id"], profile["filename"]) + if path: + return str(path.resolve()) + legacy = _crispasr_legacy_model_path(model_value) + return str(legacy.resolve()) if is_crispasr_model_file(legacy) else None + if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -566,7 +772,22 @@ def download_asr(engine, model_size="medium", hub="ms", proxy="system"): ms_cache = os.path.join(resolved, "modelscope") hf_cache = os.path.join(resolved, "huggingface", "hub") with _proxy_env(proxy): - if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: + if engine == "crispasr": + if model_size not in CRISPASR_MODEL_PROFILES: + raise ValueError(f"Invalid CrispASR model: {model_size}") + from huggingface_hub import hf_hub_download + + profile = crispasr_profile(model_size) + log.info( + f"Downloading {profile['repo_id']}:{profile['filename']} " + "from HuggingFace..." + ) + hf_hub_download( + repo_id=profile["repo_id"], + filename=profile["filename"], + cache_dir=hf_cache, + ) + elif engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine] if engine in FUNASR_LEGACY_ENGINE_ALIASES @@ -626,9 +847,15 @@ def neutralize_funasr_requirements(model_dir) -> None: def dir_size(path) -> int: + path = Path(path) + if path.is_file(): + try: + return path.stat().st_size + except (OSError, PermissionError): + return 0 total = 0 try: - for f in Path(path).rglob("*"): + for f in path.rglob("*"): if f.is_file(): total += f.stat().st_size except (OSError, PermissionError): @@ -668,6 +895,11 @@ def get_cache_entries(): if hf_path.exists(): entries.append((f"{name} (HuggingFace)", hf_path)) + for profile in CRISPASR_MODEL_PROFILES.values(): + repo_dir = _hf_repo_cache_dir(profile["repo_id"]) + if _hf_cached_file(profile["repo_id"], profile["filename"]): + entries.append((f"CrispASR {profile['display_name']} (HuggingFace)", repo_dir)) + for size in _WHISPER_SIZES: hf_path = hf_base / f"models--Systran--faster-whisper-{size}" if hf_path.exists() and is_asr_cached("whisper", size, "hf"): @@ -676,6 +908,9 @@ def get_cache_entries(): for item in list_local_faster_whisper_models(): entries.append((f"Whisper Local: {item['name']}", Path(item["path"]))) + for item in list_local_crispasr_models(): + entries.append((f"CrispASR Local: {item['name']}", Path(item["path"]))) + if torch_base.exists(): for d in sorted(torch_base.glob("snakers4_silero-vad*")): if d.is_dir(): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b899a3e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "livetranslate" +version = "0.1.0" +description = "Real-time speech recognition and translation overlay." +requires-python = ">=3.10,<3.13" +dependencies = [ + "numpy>=1.24.0", + "PyYAML>=6.0", + "httpx>=0.28.0", + "openai>=1.0.0", + "psutil>=5.9.0", + "PyAudioWPatch>=0.2.12", + "faster-whisper>=1.0.0", + "crispasr @ https://github.com/CrispStrobe/CrispASR/archive/refs/tags/v0.7.2.zip#subdirectory=python", + "editdistance-s>=1.0.0", + "omegaconf>=2.3.0", + "kaldiio>=2.18.0", + "torch-complex>=0.4.0", + "soundfile>=0.12.0", + "librosa>=0.10.0", + "jaconv>=0.3.0", + "jamo>=0.4.1", + "hydra-core>=1.3.0", + "six>=1.16.0", + "sentencepiece>=0.2.0", + "tiktoken>=0.7.0", + "transformers>=4.40.0", + "silero-vad>=5.0", + "modelscope>=1.20.0", + "huggingface_hub>=0.20.0", + "PyQt6>=6.5.0", + "pysbd", +] + +[tool.uv] +package = false diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cc0d141..0000000 --- a/requirements.txt +++ /dev/null @@ -1,40 +0,0 @@ -# Core dependencies -numpy>=1.24.0 -PyYAML>=6.0 -httpx>=0.28.0 -openai>=1.0.0 -psutil>=5.9.0 - -# Audio capture (Windows WASAPI loopback) -PyAudioWPatch>=0.2.12 - -# ASR engines -faster-whisper>=1.0.0 -editdistance-s>=1.0.0 -omegaconf>=2.3.0 -kaldiio>=2.18.0 -torch-complex>=0.4.0 -soundfile>=0.12.0 -librosa>=0.10.0 -jaconv>=0.3.0 -jamo>=0.4.1 -hydra-core>=1.3.0 -six>=1.16.0 -sentencepiece>=0.2.0 -tiktoken>=0.7.0 -transformers>=4.40.0 - -# VAD (bundles the Silero v5 model in the wheel — no GitHub/torch.hub download) -silero-vad>=5.0 - -# Model hubs -modelscope>=1.20.0 -huggingface_hub>=0.20.0 - -# UI -PyQt6>=6.5.0 - -# torch & torchaudio - install separately with CUDA support: -# pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu126 -# Blackwell GPUs (RTX 50xx, sm_120) require cu128: -# pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 diff --git a/update.bat b/update.bat index 8bef28d..28dd141 100644 --- a/update.bat +++ b/update.bat @@ -47,23 +47,35 @@ if errorlevel 1 ( ) :: Check venv -if not exist ".venv\Scripts\pip.exe" ( +if not exist ".venv\Scripts\python.exe" ( echo. echo Virtual environment not found, running install.bat... call install.bat exit /b %errorlevel% ) +:: Check uv +uv --version >nul 2>&1 +if errorlevel 1 ( + echo. + echo uv not found, running install.bat... + call install.bat + exit /b %errorlevel% +) + :: Update dependencies echo. -echo Updating dependencies... -.venv\Scripts\pip.exe install -r requirements.txt --quiet +echo Syncing dependencies... +uv sync --python .venv\Scripts\python.exe --locked --inexact --no-install-package torch --no-install-package torchaudio --quiet if errorlevel 1 ( - echo [WARN] Some dependencies failed to update. + echo [WARN] Some dependencies failed to sync. +) + +if exist "repair_torch_metadata.ps1" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "repair_torch_metadata.ps1" -PythonExe ".venv\Scripts\python.exe" ) -.venv\Scripts\pip.exe install funasr --no-deps --quiet -.venv\Scripts\pip.exe install pysbd --quiet +uv pip install --python .venv\Scripts\python.exe funasr --no-deps --quiet echo. echo ======================================== diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..27a6a14 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2106 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.13" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +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" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +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" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +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.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, +] + +[[package]] +name = "audioread" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" }, +] + +[[package]] +name = "av" +version = "17.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/e3/477fa20578c284abeda08d91b63ee9abaebc93445d8feeb989d3d444bae1/av-17.1.0.tar.gz", hash = "sha256:7f1e71ff621b66253333926f948e00faae11d855b2442133c65128bca64cdeb3", size = 4288546, upload-time = "2026-06-07T05:52:55.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/92/c9d0cea4f6f8f93f5b15a39f99d2d593f922484f22a2d98a8d482283e15b/av-17.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:19c84fd72af5ef81a20f18fbc6f9aedff9e1455e53a7062c1d4c95926d73da4e", size = 22622703, upload-time = "2026-06-07T05:51:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/57/74399770aa103ee4b5ff6da1781440c91a41901d89abb2433fe88773246e/av-17.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:19264c9bb4bee404accc7ce9ec461f2044b7f577a70234d29aafde31ed17de46", size = 18273538, upload-time = "2026-06-07T05:51:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/27c85b12e9ffa8f3f6854358b3eabcd91f3c29c7dac36843fa1376e833f4/av-17.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:22dff0ae582d10ef08c75c2150a4fd27cfc26653b54930c7c27b9f7b3aa20723", size = 34519101, upload-time = "2026-06-07T05:51:45.305Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/542d4bfd9f4aec5f3265985b9dbc6b259d45c2e668f9714e5f4e05b71e64/av-17.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90c49bc9608377d01e82e747377505419a229464873341db18202d5dddecce5a", size = 36647600, upload-time = "2026-06-07T05:51:48.57Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/63bd5c59580f38109fa4c452b29b715a20c9a5eb3a078b3c447484593c40/av-17.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cc5a5247622cb77e24c342364eb68f88c1442ddfaab60c1f1f483359d3cc7879", size = 25786289, upload-time = "2026-06-07T05:51:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/78155cef0c9f8bc13f044130192c58bf962f2c9066982ff3593afe8d27f1/av-17.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff457ed419348e5b8e8c811d341389b052c5e4d5839da3794d019b125b9fe830", size = 35599848, upload-time = "2026-06-07T05:51:54.207Z" }, + { url = "https://files.pythonhosted.org/packages/76/cb/ae1d7a735a5ad9dc502dba864c51d605cbe932a769218352fd570254c38e/av-17.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1370b11a697eb3f2555906f8ab3519b0cfe48425d7830a3996ad42e6bffafda5", size = 26776479, upload-time = "2026-06-07T05:51:56.788Z" }, + { url = "https://files.pythonhosted.org/packages/fb/40/128429b9eb0c4a2beb122ed8d04b189515df68967987c2654a2e262a5c43/av-17.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3dcd41e53f53f9a3260751d9c3c11d34e93d70d61e506c81f13dbc1e3606e07b", size = 37763744, upload-time = "2026-06-07T05:51:59.222Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/5980e7bbeeadfd7a9db8e38e9f1140a3e0c392fccc31bd7b1e4a75cf5a96/av-17.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:3453b06075c7bb973fdb6de52563f7692ff05cbc64c0bb45f4fd6e8709131f2f", size = 28126516, upload-time = "2026-06-07T05:52:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/ec/87/8036b5c781bc3639ea04ef42d4e26da253bd4bd4311d8705b6a1c8824047/av-17.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ad7b4aa011093324b7118245f50ac6db244cfe9900d4072508a5245a2b0d3f41", size = 22460847, upload-time = "2026-06-07T05:52:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/6d/af/dfdf6fc7b17814b50d0aa9e7a7e37b87be91be3890f44b0d525433cd1fd1/av-17.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:43ebbe977f19a7f2d2bd1a4e119675a0b15e05852cf7309846b6ab922ba7ffe9", size = 18159115, upload-time = "2026-06-07T05:52:06.64Z" }, + { url = "https://files.pythonhosted.org/packages/ad/13/64f6c466471cea225b8b2f4cdc51a571f8a286984b55a08d169b932fda5d/av-17.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a20658ec7d96a70e14b1196eff00b7cdd8831ac3b99868e16b8ba8b24090847", size = 33224427, upload-time = "2026-06-07T05:52:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/77/43/96b35170bf2e64e00a41748c6400ff73232dc0fc62ded283679fb07c7fe0/av-17.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f9a65d1f48b818323fb411e80358f89d77dec340b01d27c6b2dfbb9cbf4b779f", size = 35370183, upload-time = "2026-06-07T05:52:11.959Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b3/8e8b4b6498731bfbd88e8399a756543f8088f1bd33d08eab678b5aebe728/av-17.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:58f7593726437cda5bd19793027e027768450b5c4a594777bf487798a33db702", size = 24459265, upload-time = "2026-06-07T05:52:14.66Z" }, + { url = "https://files.pythonhosted.org/packages/14/ac/ceb84b7553db21f1143d817245c560d9267168e1e58b1a8eeae2b62c4d04/av-17.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bbab058bd965309f39962e53caac8126987c68c0be094fc4f9427e5615b0218f", size = 34283709, upload-time = "2026-06-07T05:52:17.389Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/4115fd84148c9a1cf365096694be6ac882fd3cd3cdb7a2f35e71fecf1631/av-17.1.0-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:9514cfda85180554c430695282faf4be3ffdf95775d8519733821244eecb58e0", size = 25397573, upload-time = "2026-06-07T05:52:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/92e52d5ed0e0b84d9d93e52b4338c2713d8a44082b8696e6516fdae7c4e4/av-17.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e1c90f85cd7431ede95b11e8e711571a896ebea433f298849c2c0f1594c8d86e", size = 36451495, upload-time = "2026-06-07T05:52:22.581Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/53a7cd34adb6a971d7e6d99663e74db286966c9db8afdca17472fdf0f98e/av-17.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:5df5c1172ef1cf65a1529d612f7da7798ce2cf82c1ff7212466b538a6cc7214c", size = 28036393, upload-time = "2026-06-07T05:52:25.657Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/cd9ae0edf2206351c1251bb94b5ec58728e42c5f6ee16c03c412f3a1bb3e/av-17.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:ee98534242a74da847af78624779ac5a3177dc7c69f956a4da9e6f0fdb37d7f6", size = 21174601, upload-time = "2026-06-07T05:52:28.077Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +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" }, +] + +[[package]] +name = "crispasr" +version = "0.5.7" +source = { url = "https://github.com/CrispStrobe/CrispASR/archive/refs/tags/v0.7.2.zip", subdirectory = "python" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { hash = "sha256:c0899c701dbe58c8847b3d5a0c298ad36077781086b11b95773ab1a6a297148b" } + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=1.20" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=7" }, +] +provides-extras = ["test"] + +[[package]] +name = "ctranslate2" +version = "4.8.0" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/6b/7329ff26a4bdfb1b395cf4dfb2b057ebe3881c5fe91f2022634689754fb8/ctranslate2-4.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e66b5dc33e94a05dfe0fcdf6ab17dfe6e0335017cc6a315de044214063b1c533", size = 1268221, upload-time = "2026-06-06T19:17:27.597Z" }, + { url = "https://files.pythonhosted.org/packages/db/3e/4e3289f428f51bbf12fd77a708a430dc1792375bd924c2c7cbaecc6e6d83/ctranslate2-4.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f25566e056d1fa9da47d6e374b2d04d6a6dfa0b631e52687a0bc4101633ecb53", size = 11925104, upload-time = "2026-06-06T19:17:29.966Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fa/b220fc42f38b8fd8f938d6621436fceea136cb5aad4c6a6f9f125e2755ba/ctranslate2-4.8.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65e64a5d79de82f302677b7ac07d3a540f893657c33fb0196824db87a17e4900", size = 16554877, upload-time = "2026-06-06T19:17:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/7a/85/d562b84d31ec28f5ddf1b49444d706b033075d42aaf656b8afea3120468f/ctranslate2-4.8.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4c8b9dc6bd8a3e79fa4109fa918e1c563b533e84a1e6c96eda6ff57cfee17f4", size = 39150038, upload-time = "2026-06-06T19:17:37.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b7/f6f6c3e5c175b10a48e949bc46c42a58f88647702ce1975e29fd6e45a5e2/ctranslate2-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b55976b0248d62aacce4e3569b0555ec2861d77b8b334ed03eaf757f51d0492d", size = 19216424, upload-time = "2026-06-06T19:17:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/70/92/ae797ea2def987a0496319c5d8988cd4aaf11a6c0c71a2fd9bbb75d13f1f/ctranslate2-4.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f56f8de6e6e036a306d427b86d86964076b614e2358b923f93fa160139ac6f5", size = 1269068, upload-time = "2026-06-06T19:17:43.67Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/ed546dd5ba660c80d83a25731313956b417d35152424f92f543aec093d0e/ctranslate2-4.8.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:59c71320788b88621be143f2795048e9f510ff690c549cffc2827831f85a1a04", size = 11926418, upload-time = "2026-06-06T19:17:45.741Z" }, + { url = "https://files.pythonhosted.org/packages/df/ef/ab22bfafc13c5d2c5a3bbbcf89ccb140a365250df29e3226dff0cfbb6748/ctranslate2-4.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2676854f374e6720600467cbde2ea2ea844fb0b6fb3e8a79795d495a3bdc7469", size = 16705990, upload-time = "2026-06-06T19:17:48.441Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c6/29d9100520d586fc5e5142ff17b2d28e4b9beeafc196982395497700fee2/ctranslate2-4.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83336d60ae04f19a30a90405040efaea1dfa0e1d95c2fe1513e53dade4681c85", size = 39349218, upload-time = "2026-06-06T19:17:52.9Z" }, + { url = "https://files.pythonhosted.org/packages/4c/2b/486dc27e200f905f3acc50ed20000ee714097616f8fe66585c29c8a4b26a/ctranslate2-4.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:402472d283d844579961b8522589401bc2c50f77f1b820783b01c25060260c3c", size = 19217441, upload-time = "2026-06-06T19:17:58.36Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f8/871b866c10d4fe4479866c4aa9c6a7ba4073dc2a657879d44411b2fb8f4c/ctranslate2-4.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ec37527dd815531209694854dd5177e763ed51d35b4b2c34da3c3ad2c9b9fd", size = 1269020, upload-time = "2026-06-06T19:18:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/316e3df68e21f79e20c277bf5c65d9825a42484ed7e3df2e6e325275ea5f/ctranslate2-4.8.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:f0b93d127a4efb6481e3d0da4c3a6ac9889a8e9d8b50f9930bc5b2401fe5e598", size = 11928718, upload-time = "2026-06-06T19:18:02.767Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d1/c4234eea5fe84733c0faed486881c7b3ebf9bc1351cb96cde7b0ecc78198/ctranslate2-4.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edfa0c1b348525d6c2713a53c90c0c50ae7a7bb2e4d59d8a59150aadba818991", size = 16880797, upload-time = "2026-06-06T19:18:05.425Z" }, + { url = "https://files.pythonhosted.org/packages/ea/34/a0ac6e2538b7d730e4537cd01ded7817dda9bc97f5b6161bbd52d16e70a3/ctranslate2-4.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:247efccc2da9a63e8bf22abb4e87789f44ec1454bdcb227b07860cdc826fc89a", size = 39526315, upload-time = "2026-06-06T19:18:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/2c3c7b110c48c36d024c5247195f2ad4fc1e34cbf482dab62ccb3898cb70/ctranslate2-4.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:06feaafe134aafa8cb2fb1fdb82e36f050bb05929dfde1a95f4fe4d7881dfc76", size = 19218985, upload-time = "2026-06-06T19:18:12.742Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "editdistance-s" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/c3/fc39711c836a942f4d462d19c70b59c1048c279d09741180792c1e0ae30c/editdistance_s-1.0.0.tar.gz", hash = "sha256:a2ea53a4f7c2fcf151ba0f35c8a258771912ba0b4068011bb76aa55a79187941", size = 5140, upload-time = "2021-03-20T07:57:30.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/d0/2d072b3b386156dada6d38c29d756b722d82ca38bfa3bacaaa6fcf765640/editdistance_s-1.0.0-cp36-abi3-macosx_10_14_x86_64.whl", hash = "sha256:8f68d57c95bef7838b1eba6d5e317f44ede020b569e13c35147a6d2d78daecf1", size = 12726, upload-time = "2021-03-20T08:04:06.899Z" }, + { url = "https://files.pythonhosted.org/packages/9b/47/9264bc1b9554ed2404f579fd226bc524d02ead9519bed09368a531983ecd/editdistance_s-1.0.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:74d275a4b0a59439e1a83a2eda5f42e3df293cdb0732c53b3609ee0af472e9fc", size = 155200, upload-time = "2021-03-20T08:00:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/15/f4/e89b31f06a2719973589ddc28b00b6b3ea67f123c97d6869e59893e1385f/editdistance_s-1.0.0-cp36-abi3-win32.whl", hash = "sha256:3e3e5a4194acaca5997e97d07c95ca70ea4b8f7a7c5ad227c865367ff1267082", size = 18445, upload-time = "2021-03-20T08:04:08.309Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8c/1013e1b262a641d8a77c2dbe94180d876b2f4945691e532717452a9926f6/editdistance_s-1.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:905ab09aa7bc6aa4aa5e800b63525377ef216ca839bcbfab05e8b3f91379844a", size = 16532, upload-time = "2021-03-20T08:04:09.646Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d9/6cdb4b1de3684e7f2589a8424ee931d0c704008a677a0f4967060b2a9c26/editdistance_s-1.0.0-cp38-abi3-macosx_12_0_arm64.whl", hash = "sha256:236936154552ba83ea2293a53889bed02e32e983cf680e7796ade1f65a5789f6", size = 12533, upload-time = "2021-11-18T17:51:41.767Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "faster-whisper" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.27.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +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" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" }, + { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/7e/fad82ad491b226e832d2da90a1a59f36acd4526cda8c726f639834754aa4/huggingface_hub-1.20.1.tar.gz", hash = "sha256:9f6d63bfbeab2d2a8357200a9bc4f18cd2c8bfac9579f792f5922e77bf6471d0", size = 859910, upload-time = "2026-06-18T22:06:53.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/b5/ff8516e74b459da3dce9567540c39f2d305ee7a2655109f6802873ff1588/huggingface_hub-1.20.1-py3-none-any.whl", hash = "sha256:274448a45c1ba6f112fe2fb168ead05574c654faa156904157a84085cfae14bd", size = 719837, upload-time = "2026-06-18T22:06:51.486Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0b/7c0d941311aadc6479ec01767edba9c8a07db1452685de3567ed3058d0c9/hydra_core-1.3.3.tar.gz", hash = "sha256:b7477ee21f08b62f71bf0126d44695c048dc7e9c0cc79e2d593b707cb1e44048", size = 3262532, upload-time = "2026-06-11T05:54:26.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/57/4e39f85347f77144d2ad12e87d5df8fb8f17023f9bd9e8c6e903a128382c/hydra_core-1.3.3-py3-none-any.whl", hash = "sha256:cf349fc393f486f250e5825592c3d0a50c0af3effd726cf8dd5b637a7cb464e3", size = 154706, upload-time = "2026-06-11T05:54:24.917Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jaconv" +version = "0.5.0" +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" }, +] + +[[package]] +name = "jamo" +version = "0.4.1" +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" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, + { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kaldiio" +version = "2.18.1" +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.6", 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 = [ + { url = "https://files.pythonhosted.org/packages/ae/e3/6c3b42233225f398f7a72988b524f654ae818cca0d441db847a2761203e9/kaldiio-2.18.1-py3-none-any.whl", hash = "sha256:397a4cd18977acaae7acabfba6807ee0a6978c620064381a266eac15b3c1a0a0", size = 29330, upload-time = "2025-03-06T15:57:50.82Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + +[[package]] +name = "librosa" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "audioread" }, + { name = "decorator" }, + { name = "joblib" }, + { name = "lazy-loader" }, + { name = "msgpack" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", 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.9.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 = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, +] + +[[package]] +name = "livetranslate" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "crispasr" }, + { name = "editdistance-s" }, + { name = "faster-whisper" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "hydra-core" }, + { name = "jaconv" }, + { name = "jamo" }, + { name = "kaldiio" }, + { name = "librosa" }, + { name = "modelscope" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "omegaconf" }, + { name = "openai" }, + { name = "psutil" }, + { name = "pyaudiowpatch" }, + { name = "pyqt6" }, + { name = "pysbd" }, + { name = "pyyaml" }, + { name = "sentencepiece" }, + { name = "silero-vad" }, + { name = "six" }, + { name = "soundfile" }, + { name = "tiktoken" }, + { name = "torch-complex" }, + { name = "transformers" }, +] + +[package.metadata] +requires-dist = [ + { name = "crispasr", url = "https://github.com/CrispStrobe/CrispASR/archive/refs/tags/v0.7.2.zip", subdirectory = "python" }, + { name = "editdistance-s", specifier = ">=1.0.0" }, + { name = "faster-whisper", specifier = ">=1.0.0" }, + { name = "httpx", specifier = ">=0.28.0" }, + { name = "huggingface-hub", specifier = ">=0.20.0" }, + { name = "hydra-core", specifier = ">=1.3.0" }, + { name = "jaconv", specifier = ">=0.3.0" }, + { name = "jamo", specifier = ">=0.4.1" }, + { name = "kaldiio", specifier = ">=2.18.0" }, + { name = "librosa", specifier = ">=0.10.0" }, + { name = "modelscope", specifier = ">=1.20.0" }, + { name = "numpy", specifier = ">=1.24.0" }, + { name = "omegaconf", specifier = ">=2.3.0" }, + { name = "openai", specifier = ">=1.0.0" }, + { name = "psutil", specifier = ">=5.9.0" }, + { name = "pyaudiowpatch", specifier = ">=0.2.12" }, + { name = "pyqt6", specifier = ">=6.5.0" }, + { name = "pysbd" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "sentencepiece", specifier = ">=0.2.0" }, + { name = "silero-vad", specifier = ">=5.0" }, + { name = "six", specifier = ">=1.16.0" }, + { name = "soundfile", specifier = ">=0.12.0" }, + { name = "tiktoken", specifier = ">=0.7.0" }, + { name = "torch-complex", specifier = ">=0.4.0" }, + { name = "transformers", specifier = ">=4.40.0" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +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" }, +] + +[[package]] +name = "modelscope" +version = "1.37.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "packaging" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/9e/9fd75db9797b9543ce78a646538a3c5537cb6adb015d128b4cbc2b4169e2/modelscope-1.37.1.tar.gz", hash = "sha256:7d9124970b4e53639bebe89f60473dee7d5a80589bf35bfbef4c839933cdaf93", size = 4594233, upload-time = "2026-05-22T05:57:09.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/f5/286dd2d21802e4954a28e73913af7bc79e882f23fcd98db2b5810fc590cf/modelscope-1.37.1-py3-none-any.whl", hash = "sha256:7f7af2dd37339188cbd3c18a858b1297b8a75222b9a0fbf176c7df8189cac39a", size = 6089945, upload-time = "2026-05-22T05:57:05.931Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +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" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, +] + +[[package]] +name = "narwhals" +version = "2.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numba" +version = "0.65.1" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1b/3c5a7daf683a95465bf23504bcd1a2d5db8cd5e5e276ca87505d020dffe9/numba-0.65.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9d993ed0a257aa4116e6f553f114004bcfdee540c7276ab8ea48f650d514c452", size = 2680870, upload-time = "2026-04-24T02:02:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/1831836814018a898e7d252aebe09c0f3ce1f26d145b68264b4ae0be6822/numba-0.65.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f098109f361681e57295f7e84d8ab2426902539a141811de0703ace52826981", size = 3739780, upload-time = "2026-04-24T02:02:13.097Z" }, + { url = "https://files.pythonhosted.org/packages/9c/1b/a813ddc81def09e257d2b1f67521982ce4b06204a87268796ffc8187271c/numba-0.65.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973fd8173f2312815e6b7aaae887c4ce8a817eeff46a4f8840b828305b75bc95", size = 3446722, upload-time = "2026-04-24T02:02:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/ee1d8b3becda384fe0552221641e05aa668a35e8a77470db4db7f6475000/numba-0.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:c63aa0c4193694026452da55d0ef9d85156c1a7a333454c103bb30dec81b7bf8", size = 2747539, upload-time = "2026-04-24T02:02:16.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/650500c2eab4534d98e9166f4298e0f3c69c742afdf24e6eabccd1f16ad8/numba-0.65.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:7020d74b19cdb8cff16506542fdd510756e28c5e7f3bd0b7f574f0f42272fcd9", size = 2680563, upload-time = "2026-04-24T02:02:18.414Z" }, + { url = "https://files.pythonhosted.org/packages/44/0b/0615dbedb98f5b32a35a53290fbdc6e22306968109278d7e58df82d7a9f6/numba-0.65.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f80ed83774b5173abd6581cd8d2165d1d38e13d2e5c8155c0c0b421784745420", size = 3745018, upload-time = "2026-04-24T02:02:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/49/aa/4361698f35bf63bff67dfe6c90493731177f48ede954f77b0588731537bc/numba-0.65.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ed425a43b0a5f9772f2f4e2dd0bbd12eabecae1af0b24efcfd4e053f012aac6", size = 3450962, upload-time = "2026-04-24T02:02:22.449Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9a/af61ec03b3116c161fd7a06b9e8a265729a8718458333e8ffbb06d9a3978/numba-0.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:df40a5028a975b9ea66f6a2a3f7abbdbd541a863070e34ed367aff21141248e4", size = 2747417, upload-time = "2026-04-24T02:02:24.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/76f8f8c5cf9adee47fdb7bbb03be8900f76f902d451d7477cf12b845e1de/numba-0.65.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ac3f1e77c352dd0ea9712732c2d8f9ca507717435eec5b5013bf138ac33c4a08", size = 2681371, upload-time = "2026-04-24T02:02:26.105Z" }, + { url = "https://files.pythonhosted.org/packages/69/47/a415af0283e4db0398104c6d1c11c9861a98dc67a7aa442a7769ed5d6196/numba-0.65.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52bc6f3ceb8fcaff9b2ae26b4c6b1e9fee39db8d355534c0fe4f39a901246b84", size = 3802467, upload-time = "2026-04-24T02:02:27.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/36/246f73ec99cfeab2f2cb2ce7d4218766cc36a2da418901223f4f4da9c813/numba-0.65.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90ca10b3463bae0bd70589726fe3c77d01d6b5fc86bee54bcdf9fb6b47c28977", size = 3502628, upload-time = "2026-04-24T02:02:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/db/9e/3c679b2ee078425b9e99a91e44f8d132a6830d8ccce5227bc5e9181aeed8/numba-0.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:5971c632be2a2351500431f46213821dba8d02b18a9f7d02fd36bd2743e41a6a", size = 2750611, upload-time = "2026-04-24T02:02:31.477Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +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" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +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" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +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" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +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" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +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" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +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" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +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" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +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" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +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" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.24.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "flatbuffers", 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 = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" }, + { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e4/5353d7e09ced4a8f473f843223fc75d726b2b5519dcefc12f22a6c92852d/onnxruntime-1.27.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8ba14a38c570087f3cdb8cfba33f7a38a1e826c1e5b29e17c28ceda0cc910016", size = 18416484, upload-time = "2026-06-15T22:43:43.894Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1f/a2117aa3f144fce88774efa37440d0ca72d0c9144854dfc0961f2b04c6fc/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb083321af8a236a84c7c140a7f4cecbfa2a987a18c07c78db471c20cd390ef", size = 16419330, upload-time = "2026-06-15T22:42:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cd/74bb804170ceb622fda9111df31a07b3024f7491472256d3a90b5391a4d2/onnxruntime-1.27.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4f7b0e90d2d212e2c2deaa6c8291616183ab815d3ec558ea12d3ac8b26d36f4", size = 18636930, upload-time = "2026-06-15T22:43:01.584Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8f/5b8e2b85e81735696887175dbaf6409f215683f5ca9d4928fbb038211d32/onnxruntime-1.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:ff050e4f6bf7f12918fa14dcb047c0b02e295f35e86d42532552be4b3d54e977", size = 13356110, upload-time = "2026-06-15T22:43:32.172Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3a/4f568de678126b6a371a93862f015a82138359decd97fcac61fc84b5b774/onnxruntime-1.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:75fbc1e1fb43a39a856c8209c544cca7817b5de7ac16b15b1bdf55d1cc67b9df", size = 13098635, upload-time = "2026-06-15T22:43:19.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/dd3a524ed93a820dff1af902d0412957ab12499953333e9daa01af5bc480/onnxruntime-1.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a14c2ce45312def86b77aea651f46565e45960cf5f0721bfdff449165086ab76", size = 18433506, upload-time = "2026-06-15T22:43:47.026Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/c3b6b17745a1997d784dadc9bd88d713d2e6721139a5a0e885b28cfb79b1/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fddce0539a4898c7bef35b052ffd37935b2190e35488eab99ce91887743ea1", size = 16438140, upload-time = "2026-06-15T22:42:40.666Z" }, + { url = "https://files.pythonhosted.org/packages/26/81/24dd9b31b0fb912ee19ca53ac1c9764bfd79d58a2ccef564eb693be831a5/onnxruntime-1.27.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c65a7438632d55dfbc8a02ee60bd6cf7dd9d1ba05a43d4b851452f32338e194", size = 18658316, upload-time = "2026-06-15T22:43:04.012Z" }, + { url = "https://files.pythonhosted.org/packages/4f/88/8ec9db1a4d126bb8b758992beb40d1249df171917d75f44a327eb5f20dda/onnxruntime-1.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:20c321cf187ba496e648acf6b4cf90b4d398b0d17c2a77fdaeba365b908cc1c1", size = 13358769, upload-time = "2026-06-15T22:43:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/fdad359dfcba7e7cd8815569b304a596531d4efa77a75d77f8b4981891a2/onnxruntime-1.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:d0d1f68868e2ef30ef70998ba9bbbc5c305e9b17041e3936751c1b8aa6aade06", size = 13104440, upload-time = "2026-06-15T22:43:22.893Z" }, +] + +[[package]] +name = "openai" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/fa/88d0c58a0c58df7e6758e66b99c5d028d5e0bb49f8812d7203940cd9dbf1/openai-2.43.0.tar.gz", hash = "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017", size = 785369, upload-time = "2026-06-17T17:06:56.06Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pooch" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "platformdirs" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/43/85ef45e8b36c6a48546af7b266592dc32d7f67837a6514d111bced6d7d75/pooch-1.9.0.tar.gz", hash = "sha256:de46729579b9857ffd3e741987a2f6d5e0e03219892c167c6578c0091fb511ed", size = 61788, upload-time = "2026-01-30T19:15:09.649Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl", hash = "sha256:f265597baa9f760d25ceb29d0beb8186c243d6607b0f60b83ecf14078dbc703b", size = 67175, upload-time = "2026-01-30T19:15:08.36Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +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/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyaudiowpatch" +version = "0.2.12.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/f6/1ab3beaae4641a6c21f2bbc6e5d95d7dcdc9cb253a406f23669fc66d996b/pyaudiowpatch-0.2.12.8-cp310-cp310-win32.whl", hash = "sha256:c5bffe533fdf37f36f58d8997f90247d46d42db2b1c4a970d9cc4637dacd322c", size = 96642, upload-time = "2026-01-14T18:19:23.49Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1b/e913831e7d7d8b5c325b2c08f00dfe93a3d6af284af64dfdcd5637e85fad/pyaudiowpatch-0.2.12.8-cp310-cp310-win_amd64.whl", hash = "sha256:7a2059464378213c0b83cdbea04a0ceacbdbd5ec791ffe29ed2ce490321aa604", size = 99091, upload-time = "2026-01-14T18:19:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0bd930258c9b81f77ec52f1edfa5a8ed44c4d6079d1253c4fee5ed766863/pyaudiowpatch-0.2.12.8-cp311-cp311-win32.whl", hash = "sha256:13903f6a78abfd745fbae82c1edda95119fce179c6f2fc95e7849a54d537dce1", size = 96645, upload-time = "2026-01-14T18:19:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/55/df/88a94ca45c8b158b52888f5010cdad2f7998d5c4a44d0794fab97937f9ec/pyaudiowpatch-0.2.12.8-cp311-cp311-win_amd64.whl", hash = "sha256:8610948499efa12b01ec861408fcc95e7bc18c1dddf0abbfed6fd2305a898f63", size = 99097, upload-time = "2026-01-14T18:19:27.409Z" }, + { url = "https://files.pythonhosted.org/packages/88/4e/ba7c938033fb25ba25f35e9f125f5c270e60c7789e65662d37424a5d9c0c/pyaudiowpatch-0.2.12.8-cp312-cp312-win32.whl", hash = "sha256:5c6f54cbee50ed3c971767cc8e85f45e94012aa3c4363b7cdd8541e9f9496d01", size = 96800, upload-time = "2026-01-14T18:19:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e1/c25c239737386a3fd85354ce35a00fbe83e993a7e51d76791022850ca12c/pyaudiowpatch-0.2.12.8-cp312-cp312-win_amd64.whl", hash = "sha256:fd163a6531c4e4969925e687ef1bc57fae8ecc1a95de7ba707a436622552d7fa", size = 99319, upload-time = "2026-01-14T18:19:31.063Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +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" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyqt6" +version = "6.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt6-qt6" }, + { name = "pyqt6-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/47/b25c13eca5bebc6505394d0223e46d7ebf0c57dcac2ed908d7d19b18ab6b/pyqt6-6.11.0.tar.gz", hash = "sha256:45dd60aa69976de1918b5ced6b4e7b6a25abd2a919ecef5fd5826ecc76718889", size = 1087430, upload-time = "2026-03-30T09:16:13.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/44/fcd3dd3f64c83c96bf9bce76ec16cca64bd9b91702c3d08fd8e3dafc73d9/pyqt6-6.11.0-cp310-abi3-macosx_10_14_universal2.whl", hash = "sha256:f7100bc7f72b12581ec479a733f4ad11b8002668e6786e8a445ab6f4d1c743d4", size = 12429735, upload-time = "2026-03-30T09:16:03.713Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a0/bd1399740dfa80c0a94d20b02d89962a31458233dcf70eaa09bfbccf3d0f/pyqt6-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8555277989fa7d114cb3c3443fd261d566909f7268ceedd41d93a5f02d37ec05", size = 8334632, upload-time = "2026-03-30T09:16:06.066Z" }, + { url = "https://files.pythonhosted.org/packages/d3/db/425b184ac2430ba1978bb507ffd285ec007a872644e2ae5df13332dbcb05/pyqt6-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:0734959955adde095af9a074213a7f73386d1bbbddfc27346b4c0621641a692e", size = 8321484, upload-time = "2026-03-30T09:16:08.135Z" }, + { url = "https://files.pythonhosted.org/packages/6f/85/dd9f03d78d87460e109e0121cd6201c5802bdd655656bf2780e964870fea/pyqt6-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:bd11b459c54dca068e988a42cf838303334f0d441b9d16d92ae6719fcb5ac6ba", size = 6844358, upload-time = "2026-03-30T09:16:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/cd/75/970b041bde4372cc6739c5ef9db1de83a6b36e788e4992e598baa35b2255/pyqt6-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:b6324e3501b19b4292c7a55b1f22e82d3e80e519e383ce4fe79b4a754c6f0288", size = 5933984, upload-time = "2026-03-30T09:16:11.817Z" }, +] + +[[package]] +name = "pyqt6-qt6" +version = "6.11.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/cb/ef930289bcd3b4be77a619d6b89ccdd0f53d753053a45f69ed590fd49777/pyqt6_qt6-6.11.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:694486f18b7ab9b1edcdb50e0c9f5cb726e096107da90ae7b0e810f18e2002b3", size = 70402836, upload-time = "2026-05-15T11:18:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/d016af2de1975a0d90c9a911e3d82b2e8c8fe899f8af746ade42186f3845/pyqt6_qt6-6.11.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd05b31a3c83111b6eb82bb472ccfe531ef823f70d085b82fd1edc5ad2553c54", size = 64167714, upload-time = "2026-05-15T11:18:48.848Z" }, + { url = "https://files.pythonhosted.org/packages/e1/be/21d0df9bde717131f4245f8801676120d466afe198c4641c9e4982cf85fe/pyqt6_qt6-6.11.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:254af349e0ef4b2fa581f86ee9d65eb797bb1d4f0c01ae5ceaa7b2446b458be9", size = 85897589, upload-time = "2026-05-15T11:19:21.864Z" }, + { url = "https://files.pythonhosted.org/packages/08/69/f6b9cafceed9790c62a7ca3044562d38545bbfcfaf222846482ac2f9bd56/pyqt6_qt6-6.11.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:039b1ab619d63d06a87cacf84b581a2b61a30c9ad5bb677553e738afe4dc433a", size = 84996419, upload-time = "2026-05-15T11:19:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f1/70e83c23bf897c7f5025aa100482f482038ef70232dc27b407659d941fbf/pyqt6_qt6-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7486c80512e823f2d3087e67f854f0556b345f4368040a853c8dc4d30fd3fe69", size = 78416766, upload-time = "2026-05-15T11:20:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/9183ec9c206a0c3cba5719f0911e88a3486167137456a0f9318f07ce000d/pyqt6_qt6-6.11.1-py3-none-win_arm64.whl", hash = "sha256:120efbedf833e5bbbc3d64ebdb139b44ff34e60f34410c7b1c2c26d230380bda", size = 59961004, upload-time = "2026-05-15T11:20:49.065Z" }, +] + +[[package]] +name = "pyqt6-sip" +version = "13.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/24/a753e1af94b9ae5b2da63d4598457308da3cdbf0838c959381db086ccc86/pyqt6_sip-13.11.1.tar.gz", hash = "sha256:869c5b48afe38e55b1ee0dd72182b0886e968cc509b98023ff50010b013ce1be", size = 92574, upload-time = "2026-03-09T13:01:35.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5b/99ffdc6382fcd3bc9da24a024ce2ba61e93c9d92ca85f99f381f2e2c8cac/pyqt6_sip-13.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2456a8a68de43f400ffd13f7f8728713434ded374e45fa7754afb9e087b2421", size = 110999, upload-time = "2026-03-09T13:01:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/48/ed/1dbe26f5757ad19601b260cebba0b40c0d868639e3253b26f80e8a1cd9f8/pyqt6_sip-13.11.1-cp310-cp310-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a1b08421967e68a821d3ea97fcdc8413671aec817a0dc46b844ab4bb2ebab66", size = 280567, upload-time = "2026-03-09T13:01:04.616Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/cded90fa556be3f6836c44bab5dd06b47664a20ffb47e7fdfa9538d7c5db/pyqt6_sip-13.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd8be768799be1a29857ea0125b1d365021a8a014c1d746f0df6b7ba0400edef", size = 304088, upload-time = "2026-03-09T13:01:02.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bb/05665db5d674c557562ec47e5703823e37c4bc943f7b3466c730a0fdeb15/pyqt6_sip-13.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:7677fa1d0e3f933838e5dd8e03ebd6cd4dfc994c9e9b24e8aabd1b3ccecb2430", size = 54032, upload-time = "2026-03-09T13:01:06.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/fa/049879f61888462099dcbab495ad16df770cca2432330cca0767ab8e87cb/pyqt6_sip-13.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0ec2128c174db352bec1c8d23a437e970e8d5a78ac50315d8dfc671fcf7a7da", size = 111056, upload-time = "2026-03-09T13:01:07.998Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/6ee861c53f3f7e6c5dd34a441d17aad1dfb3d50ce1f1a024cc9194ac3db3/pyqt6_sip-13.11.1-cp311-cp311-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6aa6c15ad3a9bb86e69119baff77b4ac17c47e55ee567abff616a4652051a6cc", size = 289930, upload-time = "2026-03-09T13:01:11.122Z" }, + { url = "https://files.pythonhosted.org/packages/ca/39/c975733d7204a594e6ae51d3a810aad539d09718aa3ceeb0dd28cb3276bd/pyqt6_sip-13.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee652b272373c4f9287625ef32ad4ec1f0755c24928dc958a870b7a928b288c", size = 315827, upload-time = "2026-03-09T13:01:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d6/c40e8ae38a6e2bce9e837b64688f55746bfdad1aa557eb733fb5e90edd7c/pyqt6_sip-13.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:98db8ed37cf08130e1ee74b8ff47a6bfb8c3cdfe826310597a630a50e47feedc", size = 54029, upload-time = "2026-03-09T13:01:12.261Z" }, + { url = "https://files.pythonhosted.org/packages/fb/63/ec8c21ef9edffb55af42c637325d72eca4ea90a73ab714aaa1429c757e85/pyqt6_sip-13.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:3af7a49dce4c35c5464309232c81cc1da5ec6074f46d2957831ee4031b8eefa6", size = 48458, upload-time = "2026-03-09T13:01:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/47598e701d284497216bf97bf8b6a69f5e61412e716c232ff2b7e6cb2100/pyqt6_sip-13.11.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ba9d362dd1e54b43bc2594f8841e1e39d24789716d28f08e5c9282af9fca342c", size = 112564, upload-time = "2026-03-09T13:01:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/95/cb/116f9b328636765f3bce97d9e10ec041c54bbe92beb0617edb86c2b615c1/pyqt6_sip-13.11.1-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0df15849946cea969d3ff2b24b76149262b6044aea2c5403e4f70c24c973a4c8", size = 299564, upload-time = "2026-03-09T13:01:17.292Z" }, + { url = "https://files.pythonhosted.org/packages/1b/be/fe2321285e8f683e705d199dbb458131f1850dc5966155a19c40100c85bb/pyqt6_sip-13.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c52b2b27fc77d9447a8dc1c6de1aaccc22d41e48697aafb2f2f20b8984bb02a5", size = 321210, upload-time = "2026-03-09T13:01:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/7d4b10f9cba1b6f581dfb4860b9d11898da55a5ed3b8a6e7a1bf9f7084d0/pyqt6_sip-13.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d1c67179c1924b28e3d7f04585639e7a7c0946f62390efc6ccf2a6206e595d3", size = 53351, upload-time = "2026-03-09T13:01:19.327Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6c4e6f21cafa4bed40d2b0c1563525b0d8bfcb5734493696f4cfd043b45f/pyqt6_sip-13.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:d83543125fe9fdb153e7e446c3b4d056d80ab5953644660633ab3f80e7784194", size = 48746, upload-time = "2026-03-09T13:01:20.248Z" }, +] + +[[package]] +name = "pysbd" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/0a/c99fb7d7e176f8b176ef19704a32e6a9c6aafdf19ef75a187f701fc15801/pysbd-0.3.4-py3-none-any.whl", hash = "sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953", size = 71082, upload-time = "2021-02-11T16:36:33.351Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "regex" +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +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 = "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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", 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/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { 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 = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", 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 = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/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" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +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" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +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" }, +] + +[[package]] +name = "silero-vad" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "torch" }, + { name = "torchaudio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/d3/e31f526482782764aa4f70e20fd4545cf2e4a81a60b6fb0f089f6d107991/silero_vad-6.2.1.tar.gz", hash = "sha256:b23062b0e39fad17b1266fc23c1e7b4290219dbe82ce08510889e32f681f4b3b", size = 28913811, upload-time = "2026-02-24T08:41:59.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2b/48566f29a8b53d856ceb1994f209122749b3fda0a733a07e82047257de7a/silero_vad-6.2.1-py3-none-any.whl", hash = "sha256:09de93c4d874bb19c53e62a47dd38be5f163cedad2b5599583231f2a84ef79cb", size = 9146242, upload-time = "2026-02-24T08:41:56.955Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +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" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soundfile" +version = "0.14.0" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" }, +] + +[[package]] +name = "soxr" +version = "1.1.0" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/11/27cebce4a108f77afea7c80545115536b45e3f11ebfb914f638fdd9ba847/soxr-1.1.0.tar.gz", hash = "sha256:9f228ae21c78fa9359ca98d8a5e8e91f30639e438e574133dace62c5b5309e44", size = 173067, upload-time = "2026-05-03T00:15:18.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/96/6b335638dd3ef4e5d50b9a0a7497e8433ab10fb45457497010074dd3c734/soxr-1.1.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9564d82f7fa6bf548e5f18bb86235dff20eea8bd30727b64d49783c95c34fb8d", size = 205270, upload-time = "2026-05-03T00:14:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/79e479b38f014757af877755c6eeea10c8750f66ef1e2231709f1d5dc7ab/soxr-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9443e5eb82152d8952422b7285692192cc7dcffa5218bb511b096203018bc273", size = 167049, upload-time = "2026-05-03T00:14:40.226Z" }, + { url = "https://files.pythonhosted.org/packages/b3/aa/52759e223bd5b4923e518d6312161887c96d42b81df5425198cf9f3371e2/soxr-1.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:588c7de1abafe59e66face9a074514658ac0398c85a774cdbb8efac131192692", size = 210589, upload-time = "2026-05-03T00:14:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/38/89/a6550d26ebeb17f83e03cf6cde2d084b8d292900a027d43a6696cbce5c5a/soxr-1.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26925618945f1a44dfbd783cc572874f0685e9ecdf46b96f4000f6b8c9c8b825", size = 245032, upload-time = "2026-05-03T00:14:43.341Z" }, + { url = "https://files.pythonhosted.org/packages/79/c6/2e47f17fa4461ba047f5f38a592f39120c21e6af8786268b0be2ef870318/soxr-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e94c713b7d96fb92841947b785bcee6606124bc852273fab70454b51bfe270", size = 176571, upload-time = "2026-05-03T00:14:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/8e/49/3e6bc84f87439f222f40b616e9a29a170f41fb564710ea510df19dc26907/soxr-1.1.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:34cc92208c3c412c046813e69da639c04a792c6a41fbfd7d909d359cd3e97a2d", size = 205699, upload-time = "2026-05-03T00:14:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/2f/94/216f46096a85b07d1e6ba7fd44491402e912a3d688cd4f36f0a600ca155f/soxr-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd30f7201eac896ebf5db7b09156e6f1a1b82601900d29d9c8449bdad8365b11", size = 167381, upload-time = "2026-05-03T00:14:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/cb/06caa463b8181ec1981bd6376d4a873748b7008193188b8cfb60391eb131/soxr-1.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1577865e993f98ffb261257c3060fa76ec3db44ed3f181b16464268000424464", size = 210938, upload-time = "2026-05-03T00:14:49.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/d5964551ca818b7f0c7ef7f3899056263b60ef098a801066350a9672ca8f/soxr-1.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3da87e3ffa3e41823d873b051c7ecb2acebd8d1b6b46b752f5facf10a0d84ab9", size = 245268, upload-time = "2026-05-03T00:14:51.422Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/371467eb86c7ba6810df0bfe9409bcd9c52ec5615b111190fafe23e4d2e1/soxr-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ae30c48ac795378cf23ba3c7c640b8ff794af714ac388b9fd6b31a40b39e6e86", size = 176779, upload-time = "2026-05-03T00:14:53.09Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f3da7973b5f1b05d2d7e94d5376b881dcbc05297900cae6c3d33d95b209b/soxr-1.1.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:e0e09fa633ce2e67df08b298afced4d184f6e753fc330f241022250f1d0d61da", size = 204124, upload-time = "2026-05-03T00:14:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/dc/200013a74641f8774664bbcd2346c695c05c2e300ea792adcb40a293eed0/soxr-1.1.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6a7ad82b8d5f3fcc04b1d2ca055562b96af571e1d4fa7c6c61d0fb509ac43b4", size = 165457, upload-time = "2026-05-03T00:14:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/88/2b/2e5eba817a762a2ec589ff165b8bc5955b25a0ad140045f7cd8e45410543/soxr-1.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf98c0d7b7d5ef5bf072fee8d3020e8b664f2d195933ea7bc5089267c2e22a06", size = 206529, upload-time = "2026-05-03T00:14:57.646Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f1/0e55195893228609c9a08c3b13b7a83a46c3a992cd00d3304f0f320cfb07/soxr-1.1.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b033078e86f3c4a658e5697fac8995764fad9e799563616b630136b613167f1", size = 240413, upload-time = "2026-05-03T00:14:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4d/621e4150e4815246ad552d215a8a294a90143fedd19ee442cf82d3b3abc8/soxr-1.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:6ae2a174bffea94e8ead857dad85999d3f49f091774dbad5b046c0417d7092f4", size = 174357, upload-time = "2026-05-03T00:15:00.724Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { 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'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, +] + +[[package]] +name = "torch-complex" +version = "0.4.4" +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.6", 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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/c5/9b4d756a7ada951e9b17dcc636f98ed1073c737ae809b150ef408afb6298/torch_complex-0.4.4-py3-none-any.whl", hash = "sha256:6ab4ecd4f3a16e3adb70a7f7cd2e769a9dfd07d7a8e27d04ff9c621ebbe34b13", size = 9125, upload-time = "2024-06-28T07:10:26.651Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d9/357eb5fe4e19a861e6fa1af4d9f535e8fa8692336e6cf436e8a21262e054/torchaudio-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ebb59c694909eccb5d61b7cc199d297692012c43286e36d92983aa7bad7586d", size = 684145, upload-time = "2026-03-23T18:13:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/90de77e73f395bba2fe477f8e82e4ae1d14d6452a706838765e850a5e80c/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be7ad472acb16d16e98c005f0219b0db06a47dfe8f7b4d177062e1638f871e3b", size = 1626521, upload-time = "2026-03-23T18:13:40.98Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/5757ed7d8d11a6c14336bcb54e63980979f00005555fec80fb4aa4de5eff/torchaudio-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5847fe2022b17c6580aeb39c8797a443411cc09edfd9183cd50ac1a3b8ccf97c", size = 1771929, upload-time = "2026-03-23T18:13:43.432Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f4/8ce2417eac66296e45b7aaa69858403fb6a52b1323f8635ec37b4b0f1fa3/torchaudio-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e2da1df4f6fe885c46db350a0dc90a0dff4b54541dff8846faa904d255e2bfe", size = 328661, upload-time = "2026-03-23T18:13:45.77Z" }, + { url = "https://files.pythonhosted.org/packages/94/77/0eec7f175d88f312296bd5b11c23bd58da37c1021f53da3db4df449ce3ee/torchaudio-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:492dd64645e9d0bb843e94f1d9a4d1e31426262ffc594fafecc1697df9df5eb9", size = 684142, upload-time = "2026-03-23T18:13:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f9/6f7ebe071b44592c85269762b55b63ab0a091b5f479f73544738f7564a1e/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:73dab4841f94d888bc7c2aed7b5547c643edc974306919fe1adfb65d57cccf4b", size = 1626527, upload-time = "2026-03-23T18:13:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/17408e0d154d0c894537a88dcbadc48e8ad3b6e1ef4a1dabda5d40245ee0/torchaudio-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1a07ec72fd6f26a588c39b5f029e0130d16bb40bc4221635580bf8fb18fcbc80", size = 1771930, upload-time = "2026-03-23T18:13:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/c9/75/b6d03fc75b409bdaec597274d1bdd4213db716ed16f6801386b31d59c551/torchaudio-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb59ba4452bbbe95d75ad3ef18df9824955625f36698ce9a5998a4a9f3c1ba1d", size = 328658, upload-time = "2026-03-23T18:13:44.545Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/77658817acacd01a72b714440c62f419efc4d90170e704e8e7a2c0918988/torchaudio-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1cf1acc883bee9cb906a933572fed6a8a933f86ef34e9ea7d803f72317e8c1b", size = 684226, upload-time = "2026-03-23T18:13:40.023Z" }, + { url = "https://files.pythonhosted.org/packages/78/28/c7adc053039f286c2aca0038b766cbe3294e66fec6b29a820e95128f9ede/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:bc653defca1c16154398517a1adc98d0fb7f1dd08e58ced217558d213c2c6e29", size = 1626670, upload-time = "2026-03-23T18:13:42.162Z" }, + { url = "https://files.pythonhosted.org/packages/88/d8/d6d0f896e064aa67377484efef4911cdcc07bce2929474e1417cc0af18c2/torchaudio-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6503c0bdb29daf2e6281bb70ea2dfe2c3553b782b619eb5d73bdadd8a3f7cecf", size = 1771992, upload-time = "2026-03-23T18:13:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/941277ecc39f7a0a169d554302a1f1afd87c1d94a8aec828891916cea59a/torchaudio-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:478110f981e5d40a8d82221732c57a56c85a1d5895fb8fe646e86ee15eded3bd", size = 328663, upload-time = "2026-03-23T18:13:19.218Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, +] + +[[package]] +name = "transformers" +version = "5.12.1" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/7c/8240f612819718100a9346dc28dea6a11370c3ca9c8c6eabadd3dea4ef29/transformers-5.12.1.tar.gz", hash = "sha256:679ee731c8225347889ad4fb3b2c926a62e9da3b7d284e9d12c791da7272466b", size = 8924054, upload-time = "2026-06-15T17:27:50.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/56/bbd60dd8668055803bf8ba55a81f9b8a8b31497f620109a9671d26a2076d/transformers-5.12.1-py3-none-any.whl", hash = "sha256:2a5e109d2021265df7098ffbb738295acaf5ad256f12cbc586db2ea4dcbb1a8a", size = 11150587, upload-time = "2026-06-15T17:27:46.679Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +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" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From 267650e4f5a1ec1897023bbfc3b892b62fc06208 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 02:01:51 +0800 Subject: [PATCH 02/13] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=20FunASR=20requ?= =?UTF-8?q?irements.txt=20=E8=A7=84=E9=81=BF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 uv 和 pyproject.toml/uv.lock 管理依赖后, FunASR 模型目录中的 requirements.txt 已不再作为运行时依赖来源, 旧的加载前重命名逻辑失去意义。 移除 SenseVoice、FunASR Nano 加载前和模型下载后的 neutralize_funasr_requirements 调用, 同时删除对应工具函数, 避免继续保留已废弃的模型文件改写路径。 --- asr_funasr_nano.py | 2 -- asr_sensevoice.py | 2 -- model_manager.py | 23 ----------------------- 3 files changed, 27 deletions(-) diff --git a/asr_funasr_nano.py b/asr_funasr_nano.py index d5ccf70..d14e4ec 100644 --- a/asr_funasr_nano.py +++ b/asr_funasr_nano.py @@ -27,7 +27,6 @@ def __init__(self, device="cuda", hub="ms", engine_type="funasr-nano"): from model_manager import ( ASR_MODEL_IDS, get_local_model_path, - neutralize_funasr_requirements, ) model_name = ASR_MODEL_IDS[engine_type] @@ -36,7 +35,6 @@ def __init__(self, device="cuda", hub="ms", engine_type="funasr-nano"): if local: self._ensure_qwen_weights(local) - neutralize_funasr_requirements(local) prev_cwd = os.getcwd() if local: diff --git a/asr_sensevoice.py b/asr_sensevoice.py index 7dfb850..3e56403 100644 --- a/asr_sensevoice.py +++ b/asr_sensevoice.py @@ -30,12 +30,10 @@ def __init__(self, model_name=None, device="cuda", hub="ms", pad_seconds=None): from model_manager import ( get_local_model_path, asr_model_id, - neutralize_funasr_requirements, ) local = get_local_model_path("sensevoice", hub=hub) model = local or model_name or asr_model_id("sensevoice", hub) - neutralize_funasr_requirements(local) self._set_precision(device) model_kwargs = { "model": model, diff --git a/model_manager.py b/model_manager.py index 9759654..1ae8844 100644 --- a/model_manager.py +++ b/model_manager.py @@ -805,9 +805,6 @@ def download_asr(engine, model_size="medium", hub="ms", proxy="system"): model_id = funasr_model_id(model_key, "hf") log.info(f"Downloading {model_id} from HuggingFace...") snapshot_download(repo_id=model_id, cache_dir=hf_cache) - neutralize_funasr_requirements( - get_local_model_path("funasr", hub=hub, funasr_model=model_key) - ) elif engine == "anime-whisper": # HF-only, ignore hub setting from huggingface_hub import snapshot_download @@ -826,26 +823,6 @@ def download_asr(engine, model_size="medium", hub="ms", proxy="system"): log.info(f"ASR model downloaded: {engine}") -def neutralize_funasr_requirements(model_dir) -> None: - """Skip FunASR's load-time `pip install -r requirements.txt`. - - With trust_remote_code=True, FunASR detects requirements.txt in the model - dir and runs pip in a subprocess whose output is swallowed (PIPE). On a slow - or proxy-blocked PyPI this hangs indefinitely with no log output, and it can - pull heavy unused deps (e.g. gradio). All real deps already live in the venv, - so rename the file out of the way to make the check miss. - """ - if not model_dir: - return - req = Path(model_dir) / "requirements.txt" - if req.exists(): - try: - req.replace(req.with_name("requirements.txt.bundled")) - log.info(f"Skipped FunASR requirements install: {req}") - except OSError as exc: - log.warning(f"Failed to neutralize {req}: {exc}") - - def dir_size(path) -> int: path = Path(path) if path.is_file(): From edc846462d3ddc71366c10426f7401df26c96ae4 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 04:35:08 +0800 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=E8=A6=81=E6=B1=82=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E9=80=89=E6=8B=A9=20CrispASR=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 CrispASR 默认模型改为空值,并在控制面板下载区域提供模型选择入口,避免未选择时自动落到内置默认模型。 新增中英文占位和未选择提示,在未选择 CrispASR 模型时禁用下载按钮并跳过预下载。 运行时在 CrispASR 模型为空或无效时保留当前 ASR worker,避免以无效模型重新加载;本地 CrispASR 模型列表继续用真实路径展示,同时用解析路径去重。 --- config.yaml | 4 +- control_panel.py | 104 +++++++++++++++++++++++------------------------ i18n/en.yaml | 2 + i18n/zh.yaml | 2 + main.py | 36 ++++++++++++---- model_manager.py | 13 +++--- 6 files changed, 90 insertions(+), 71 deletions(-) diff --git a/config.yaml b/config.yaml index 2a6cb98..b5c3bbb 100644 --- a/config.yaml +++ b/config.yaml @@ -15,8 +15,8 @@ asr: model_size: "medium" # FunASR model: sensevoice-small, funasr-nano-2512, funasr-mlt-nano-2512 funasr_model: "sensevoice-small" - # CrispASR GGUF model key or local .gguf/.bin path - crispasr_model: "parakeet-tdt-0.6b-v3-q4_k" + # CrispASR GGUF model key or local .gguf/.bin path. Empty means user has not selected a model yet. + crispasr_model: "" crispasr_backend: "auto" crispasr_gpu_backend: "auto" crispasr_device_index: 0 diff --git a/control_panel.py b/control_panel.py index a821602..9efb30f 100644 --- a/control_panel.py +++ b/control_panel.py @@ -34,7 +34,6 @@ ModelEditDialog, ) from model_manager import ( - DEFAULT_CRISPASR_MODEL, DEFAULT_FUNASR_MODEL, MODELS_DIR, _WHISPER_SIZES, @@ -47,7 +46,6 @@ list_local_crispasr_models, list_local_faster_whisper_models, migrate_funasr_settings, - normalize_crispasr_model_key, normalize_funasr_model_key, resolve_custom_crispasr_model, resolve_custom_whisper_model, @@ -120,9 +118,7 @@ def __init__(self, config, saved_settings=None): "funasr_model": config["asr"].get( "funasr_model", DEFAULT_FUNASR_MODEL ), - "crispasr_model": config["asr"].get( - "crispasr_model", DEFAULT_CRISPASR_MODEL - ), + "crispasr_model": config["asr"].get("crispasr_model", ""), "crispasr_backend": config["asr"].get("crispasr_backend", "auto"), "crispasr_gpu_backend": config["asr"].get( "crispasr_gpu_backend", "auto" @@ -184,11 +180,8 @@ def __init__(self, config, saved_settings=None): ) self._current_settings.setdefault( "crispasr_model", - config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL), + config["asr"].get("crispasr_model", ""), ) - crispasr_setting = self._current_settings.get("crispasr_model") - if not crispasr_setting: - self._current_settings["crispasr_model"] = DEFAULT_CRISPASR_MODEL self._current_settings.setdefault( "crispasr_backend", config["asr"].get("crispasr_backend", "auto") ) @@ -318,16 +311,6 @@ def _create_vad_tab(self): asr_layout.addWidget(self._funasr_model_label, 3, 0) asr_layout.addWidget(self._funasr_model_combo, 3, 1) - self._crispasr_model_label = QLabel(t("label_crispasr_model")) - self._crispasr_model_combo = QComboBox() - saved_crispasr_model = s.get("crispasr_model", DEFAULT_CRISPASR_MODEL) - self._populate_crispasr_models(saved_crispasr_model) - self._crispasr_model_combo.currentIndexChanged.connect( - self._on_crispasr_setting_changed - ) - asr_layout.addWidget(self._crispasr_model_label, 4, 0) - asr_layout.addWidget(self._crispasr_model_combo, 4, 1) - self._crispasr_gpu_label = QLabel(t("label_crispasr_gpu_backend")) self._crispasr_gpu_backend = QComboBox() self._crispasr_gpu_backend.addItems(["Auto", "CUDA", "Vulkan", "CPU"]) @@ -335,20 +318,20 @@ def _create_vad_tab(self): gpu_idx = {"auto": 0, "cuda": 1, "vulkan": 2, "cpu": 3}.get(gpu_backend, 0) self._crispasr_gpu_backend.setCurrentIndex(gpu_idx) self._crispasr_gpu_backend.currentIndexChanged.connect( - self._on_crispasr_setting_changed + self._on_crispasr_runtime_setting_changed ) - asr_layout.addWidget(self._crispasr_gpu_label, 5, 0) - asr_layout.addWidget(self._crispasr_gpu_backend, 5, 1) + asr_layout.addWidget(self._crispasr_gpu_label, 4, 0) + asr_layout.addWidget(self._crispasr_gpu_backend, 4, 1) self._crispasr_device_label = QLabel(t("label_crispasr_device_index")) self._crispasr_device_index = QSpinBox() self._crispasr_device_index.setRange(0, 16) self._crispasr_device_index.setValue(int(s.get("crispasr_device_index", 0) or 0)) self._crispasr_device_index.valueChanged.connect( - self._on_crispasr_setting_changed + self._on_crispasr_runtime_setting_changed ) - asr_layout.addWidget(self._crispasr_device_label, 6, 0) - asr_layout.addWidget(self._crispasr_device_index, 6, 1) + asr_layout.addWidget(self._crispasr_device_label, 5, 0) + asr_layout.addWidget(self._crispasr_device_index, 5, 1) self._crispasr_punc_label = QLabel(t("label_crispasr_punc_model")) self._crispasr_punc_model = QComboBox() @@ -366,19 +349,19 @@ def _create_vad_tab(self): if punc_idx >= 0: self._crispasr_punc_model.setCurrentIndex(punc_idx) self._crispasr_punc_model.currentIndexChanged.connect( - self._on_crispasr_setting_changed + self._on_crispasr_runtime_setting_changed ) - asr_layout.addWidget(self._crispasr_punc_label, 7, 0) - asr_layout.addWidget(self._crispasr_punc_model, 7, 1) + asr_layout.addWidget(self._crispasr_punc_label, 6, 0) + asr_layout.addWidget(self._crispasr_punc_model, 6, 1) self._crispasr_unified_memory = QCheckBox(t("label_crispasr_unified_memory")) self._crispasr_unified_memory.setChecked( bool(s.get("crispasr_unified_memory", True)) ) self._crispasr_unified_memory.toggled.connect( - self._on_crispasr_setting_changed + self._on_crispasr_runtime_setting_changed ) - asr_layout.addWidget(self._crispasr_unified_memory, 8, 1) + asr_layout.addWidget(self._crispasr_unified_memory, 7, 1) self._whisper_pad_label = QLabel(t("label_whisper_padding")) self._whisper_pad_seconds = QDoubleSpinBox() @@ -393,8 +376,8 @@ def _create_vad_tab(self): self._whisper_pad_seconds.setSuffix(" s") self._whisper_pad_seconds.setSpecialValueText(t("whisper_padding_off")) self._whisper_pad_seconds.setToolTip(t("whisper_padding_tooltip")) - asr_layout.addWidget(self._whisper_pad_label, 9, 0) - asr_layout.addWidget(self._whisper_pad_seconds, 9, 1) + asr_layout.addWidget(self._whisper_pad_label, 8, 0) + asr_layout.addWidget(self._whisper_pad_seconds, 8, 1) self._whisper_pad_seconds.valueChanged.connect(self._auto_save) self._sensevoice_pad_label = QLabel(t("label_sensevoice_padding")) @@ -410,8 +393,8 @@ def _create_vad_tab(self): self._sensevoice_pad_seconds.setSuffix(" s") self._sensevoice_pad_seconds.setSpecialValueText(t("sensevoice_padding_off")) self._sensevoice_pad_seconds.setToolTip(t("sensevoice_padding_tooltip")) - asr_layout.addWidget(self._sensevoice_pad_label, 10, 0) - asr_layout.addWidget(self._sensevoice_pad_seconds, 10, 1) + asr_layout.addWidget(self._sensevoice_pad_label, 9, 0) + asr_layout.addWidget(self._sensevoice_pad_seconds, 9, 1) self._sensevoice_pad_seconds.valueChanged.connect(self._auto_save) self._audio_device = QComboBox() @@ -433,8 +416,8 @@ def _create_vad_tab(self): self._audio_device.setCurrentIndex(idx) else: self._audio_device.setCurrentIndex(1) # system default - asr_layout.addWidget(QLabel(t("label_audio")), 11, 0) - asr_layout.addWidget(self._audio_device, 11, 1) + asr_layout.addWidget(QLabel(t("label_audio")), 10, 0) + asr_layout.addWidget(self._audio_device, 10, 1) self._audio_device.currentIndexChanged.connect(self._auto_save) self._mic_device = QComboBox() @@ -455,16 +438,16 @@ def _create_vad_tab(self): idx = self._mic_device.findText(saved_mic) if idx >= 0: self._mic_device.setCurrentIndex(idx) - asr_layout.addWidget(QLabel(t("label_mic")), 12, 0) - asr_layout.addWidget(self._mic_device, 12, 1) + asr_layout.addWidget(QLabel(t("label_mic")), 11, 0) + asr_layout.addWidget(self._mic_device, 11, 1) self._mic_device.currentIndexChanged.connect(self._auto_save) self._hub_combo = QComboBox() self._hub_combo.addItems([t("hub_modelscope"), t("hub_huggingface")]) saved_hub = s.get("hub", "ms") self._hub_combo.setCurrentIndex(0 if saved_hub == "ms" else 1) - asr_layout.addWidget(QLabel(t("label_hub")), 13, 0) - asr_layout.addWidget(self._hub_combo, 13, 1) + asr_layout.addWidget(QLabel(t("label_hub")), 12, 0) + asr_layout.addWidget(self._hub_combo, 12, 1) self._hub_combo.currentIndexChanged.connect(self._auto_save) self._ui_lang_combo = QComboBox() @@ -473,8 +456,8 @@ def _create_vad_tab(self): saved_lang = s.get("ui_lang", get_lang()) self._ui_lang_combo.setCurrentIndex(0 if saved_lang == "en" else 1) - asr_layout.addWidget(QLabel(t("label_ui_lang")), 14, 0) - asr_layout.addWidget(self._ui_lang_combo, 14, 1) + asr_layout.addWidget(QLabel(t("label_ui_lang")), 13, 0) + asr_layout.addWidget(self._ui_lang_combo, 13, 1) self._ui_lang_combo.currentIndexChanged.connect(self._on_ui_lang_changed) layout.addWidget(asr_group) @@ -502,6 +485,13 @@ def _create_vad_tab(self): self._crispasr_group = QGroupBox(t("group_download_crispasr")) crispasr_layout = QHBoxLayout(self._crispasr_group) + self._crispasr_model_combo = QComboBox() + saved_crispasr_model = s.get("crispasr_model", "") + self._populate_crispasr_models(saved_crispasr_model) + self._crispasr_model_combo.currentIndexChanged.connect( + self._on_crispasr_model_changed + ) + crispasr_layout.addWidget(self._crispasr_model_combo) self._crispasr_status = QLabel("") self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") crispasr_layout.addWidget(self._crispasr_status, 1) @@ -1222,9 +1212,7 @@ def _on_asr_engine_changed(self, index): if hasattr(self, "_funasr_model_combo"): self._funasr_model_label.setVisible(is_funasr) self._funasr_model_combo.setVisible(is_funasr) - if hasattr(self, "_crispasr_model_combo"): - self._crispasr_model_label.setVisible(is_crispasr) - self._crispasr_model_combo.setVisible(is_crispasr) + if hasattr(self, "_crispasr_gpu_backend"): self._crispasr_gpu_label.setVisible(is_crispasr) self._crispasr_gpu_backend.setVisible(is_crispasr) self._crispasr_device_label.setVisible(is_crispasr) @@ -1260,12 +1248,7 @@ def _on_funasr_model_changed(self): def _selected_crispasr_model(self) -> str: value = self._crispasr_model_combo.currentData() - value = str(value) if value else "" - if value in dict(crispasr_model_options()): - return normalize_crispasr_model_key(value) - if value: - return value - return DEFAULT_CRISPASR_MODEL + return str(value) if value is not None else "" def _selected_crispasr_gpu_backend(self) -> str: return ["auto", "cuda", "vulkan", "cpu"][self._crispasr_gpu_backend.currentIndex()] @@ -1274,8 +1257,12 @@ def _selected_crispasr_punc_model(self) -> str: value = self._crispasr_punc_model.currentData() return str(value) if value else "auto" - def _on_crispasr_setting_changed(self): + def _on_crispasr_model_changed(self): self._current_settings["crispasr_model"] = self._selected_crispasr_model() + self._update_crispasr_status() + self._auto_save() + + def _on_crispasr_runtime_setting_changed(self): self._current_settings["crispasr_gpu_backend"] = ( self._selected_crispasr_gpu_backend() ) @@ -1288,7 +1275,6 @@ def _on_crispasr_setting_changed(self): self._current_settings["crispasr_unified_memory"] = ( self._crispasr_unified_memory.isChecked() ) - self._update_crispasr_status() self._auto_save() def _selected_whisper_model(self) -> str: @@ -1298,6 +1284,7 @@ def _selected_whisper_model(self) -> str: def _populate_crispasr_models(self, saved_value: str): self._crispasr_model_combo.clear() builtin_models = dict(crispasr_model_options()) + self._crispasr_model_combo.addItem(t("crispasr_model_placeholder"), "") for key, display_name in crispasr_model_options(): self._crispasr_model_combo.addItem(display_name, key) @@ -1311,7 +1298,9 @@ def _populate_crispasr_models(self, saved_value: str): idx, item["path"], Qt.ItemDataRole.ToolTipRole ) - if saved_value in builtin_models: + if not saved_value: + selected = "" + elif saved_value in builtin_models: selected = saved_value else: selected = resolve_custom_crispasr_model(saved_value) or saved_value @@ -1418,6 +1407,11 @@ def _update_crispasr_status(self): from model_manager import is_asr_cached, crispasr_profile model_key = self._selected_crispasr_model() + if not model_key: + self._crispasr_status.setText(t("crispasr_select_model")) + self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") + self._crispasr_dl_btn.setEnabled(False) + return hub = self._current_settings.get("hub", "ms") cached = is_asr_cached("crispasr", model_key, hub) if model_key not in dict(crispasr_model_options()): @@ -1443,6 +1437,8 @@ def _download_crispasr(self): from model_manager import is_asr_cached, get_missing_models model_key = self._selected_crispasr_model() + if not model_key: + return hub = self._current_settings.get("hub", "ms") if is_asr_cached("crispasr", model_key, hub): return diff --git a/i18n/en.yaml b/i18n/en.yaml index 4792246..6ac52ec 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -159,6 +159,8 @@ whisper_missing_local: "Missing" whisper_invalid_local: "Local model unavailable" group_download_crispasr: "CrispASR Model" btn_download_crispasr: "Download" +crispasr_model_placeholder: "Select CrispASR model" +crispasr_select_model: "Select a model" crispasr_already_cached: "Already cached" crispasr_local_prefix: "Local" crispasr_local_ready: "Local model" diff --git a/i18n/zh.yaml b/i18n/zh.yaml index 9b1fec0..0bf4709 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -159,6 +159,8 @@ whisper_missing_local: "缺失" whisper_invalid_local: "本地模型不可用" group_download_crispasr: "CrispASR 模型" btn_download_crispasr: "下载" +crispasr_model_placeholder: "请选择 CrispASR 模型" +crispasr_select_model: "请选择模型" crispasr_already_cached: "已缓存" crispasr_local_prefix: "本地" crispasr_local_ready: "本地模型" diff --git a/main.py b/main.py index cec45f9..bbd870d 100644 --- a/main.py +++ b/main.py @@ -17,8 +17,8 @@ from datetime import datetime from model_manager import ( + CRISPASR_MODEL_PROFILES, DEFAULT_FUNASR_MODEL, - DEFAULT_CRISPASR_MODEL, apply_cache_env, crispasr_profile, funasr_display_name, @@ -31,7 +31,6 @@ local_crispasr_display_name, local_faster_whisper_display_name, migrate_funasr_settings, - normalize_crispasr_model_key, normalize_asr_engine_selection, normalize_funasr_model_key, resolve_custom_crispasr_model, @@ -188,9 +187,7 @@ def __init__(self, config): self._funasr_model_key = normalize_funasr_model_key( config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL) ) - self._crispasr_model_key = normalize_crispasr_model_key( - config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL) - ) + self._crispasr_model_key = str(config["asr"].get("crispasr_model", "") or "") self._asr_lock = threading.RLock() self._vad_lock = threading.Lock() self._target_language = config["translation"]["target_language"] @@ -463,6 +460,7 @@ def _switch_asr_engine(self, engine_type: str): "crispasr_model", self._config["asr"].get("crispasr_model", self._crispasr_model_key), ) + crispasr_model = str(crispasr_model or "") crispasr_model_path_value = None crispasr_backend = settings.get( "crispasr_backend", self._config["asr"].get("crispasr_backend", "auto") @@ -495,16 +493,38 @@ def _switch_asr_engine(self, engine_type: str): elif engine_type == "funasr": cache_model_key = funasr_model elif engine_type == "crispasr": + if not crispasr_model: + log.warning("CrispASR model is not selected; keeping current ASR worker") + with self._asr_lock: + if not ( + self._asr_ready + and self._asr is not None + and self._asr.status == "ready" + ): + self._asr_ready = False + return custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) if custom_crispasr_path: cache_model_key = custom_crispasr_path crispasr_model_path_value = custom_crispasr_path - else: - crispasr_model = normalize_crispasr_model_key(crispasr_model) + elif crispasr_model in CRISPASR_MODEL_PROFILES: cache_model_key = crispasr_model crispasr_model_path_value = get_local_model_path( "crispasr", hub=hub, funasr_model=crispasr_model ) + else: + log.warning( + "CrispASR model is invalid or unavailable: %s; keeping current ASR worker", + crispasr_model, + ) + with self._asr_lock: + if not ( + self._asr_ready + and self._asr is not None + and self._asr.status == "ready" + ): + self._asr_ready = False + return compute = self._config["asr"]["compute_type"] if engine_type == "whisper": @@ -1645,7 +1665,7 @@ def main(): elif current_engine == "crispasr": startup_model_key = saved.get( "crispasr_model", - config["asr"].get("crispasr_model", DEFAULT_CRISPASR_MODEL), + config["asr"].get("crispasr_model", ""), ) else: startup_model_key = saved.get("whisper_model_size", config["asr"]["model_size"]) diff --git a/model_manager.py b/model_manager.py index 1ae8844..182bd5e 100644 --- a/model_manager.py +++ b/model_manager.py @@ -283,7 +283,7 @@ def _custom_crispasr_path(value) -> Path | None: def resolve_custom_crispasr_model(value) -> str | None: path = _custom_crispasr_path(value) if path and is_crispasr_model_file(path): - return str(path.resolve()) + return str(path.absolute()) return None @@ -455,25 +455,24 @@ def list_local_crispasr_models() -> list[dict]: for path in files: if _is_builtin_crispasr_cache(path): continue - if _is_hf_hub_cache(path): - continue if path.name == "model.bin" and is_faster_whisper_model_dir(path.parent): continue if not is_crispasr_model_file(path): continue try: - resolved = str(path.resolve()) + identity = str(path.resolve()) + model_path = str(path.absolute()) except OSError: continue - if resolved in seen: + if identity in seen: continue - seen.add(resolved) + seen.add(identity) name = path.name name_counts[name] = name_counts.get(name, 0) + 1 if name_counts[name] > 1: name = f"{path.stem} ({path.parent.name}){path.suffix}" - entries.append({"name": name, "path": resolved}) + entries.append({"name": name, "path": model_path}) entries.sort(key=lambda item: item["name"].lower()) return entries From 26ff7dd76da05fb19cb69df31d8884361f625556 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 05:19:55 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=E6=94=B6=E5=8F=A3=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=AD=98=E5=82=A8=E5=92=8C=E5=85=AC=E5=BC=80=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=8A=B6=E6=80=81=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 settings_store.py,集中 user_settings.json 的加载、保存、默认值补齐和旧设置迁移 - 保留 control_panel 旧设置读写 wrapper,同时增加模型、目标语言、ASR 语言、字幕模式等公开同步方法 - main.py 改用 panel、overlay、AudioCapture、VADProcessor 的公开 API,减少对私有字段的直接访问 - 为 AudioCapture 和 VADProcessor 增加公开状态属性与 buffer 统计接口 - 清理默认配置和首次启动对话框中的硬编码 API Key,并补充 README 凭据存放说明 --- README.md | 2 + README_zh.md | 2 + audio_capture.py | 14 ++++ config.yaml | 2 +- control_panel.py | 191 ++++++++++++++++---------------------------- main.py | 143 +++++++++++---------------------- settings_store.py | 137 +++++++++++++++++++++++++++++++ subtitle_overlay.py | 32 ++++++++ vad_processor.py | 22 +++++ 9 files changed, 323 insertions(+), 222 deletions(-) create mode 100644 settings_store.py diff --git a/README.md b/README.md index cf0a492..3f7cdb4 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Settings → Translation tab: | Model | `deepseek-chat` | | Proxy | `none` / `system` / custom URL | +Real API keys are stored in `user_settings.json`, which is git-ignored. Keep `config.yaml` free of real credentials. + ## Architecture ``` diff --git a/README_zh.md b/README_zh.md index 550f628..b03f3c5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -107,6 +107,8 @@ uv pip install funasr --no-deps | Model | `deepseek-chat` | | 代理 | `none` / `system` / 自定义地址 | +真实 API Key 会保存在已被 git 忽略的 `user_settings.json` 中,不要把真实凭据写入 `config.yaml`。 + ## 架构 ``` diff --git a/audio_capture.py b/audio_capture.py index 7657e5a..141a9e6 100644 --- a/audio_capture.py +++ b/audio_capture.py @@ -82,6 +82,20 @@ def __init__(self, device=None, sample_rate=16000, chunk_duration=0.5): self._mic_restart_event = threading.Event() self._mic_buf = np.array([], dtype=np.float32) + @property + def device_name(self): + """Configured loopback device name. None means system default.""" + return self._device_name + + @property + def current_device_name(self): + """Actual opened loopback device name, if a stream is active.""" + return self._current_device_name + + @property + def is_loopback_disabled(self) -> bool: + return self._loopback_disabled + def _get_wasapi_info(self): for i in range(self._pa.get_host_api_count()): info = self._pa.get_host_api_info_by_index(i) diff --git a/config.yaml b/config.yaml index b5c3bbb..666e64c 100644 --- a/config.yaml +++ b/config.yaml @@ -42,7 +42,7 @@ asr: translation: # OpenAI-compatible API settings api_base: "http://127.0.0.1:1234/v1" - api_key: "sk-lm-tHzDfNGm:dgxlip7eebn3HIMxivqN" + api_key: "" model: "hunyuan-mt-chimera-7b" # Target language for translation target_language: "zh" diff --git a/control_panel.py b/control_panel.py index 9efb30f..9043f0b 100644 --- a/control_panel.py +++ b/control_panel.py @@ -1,8 +1,6 @@ -import json import logging import os import threading -from pathlib import Path from PyQt6.QtCore import Qt, QTimer, pyqtSignal from PyQt6.QtGui import QFont @@ -52,34 +50,21 @@ ) from i18n import t, LANGUAGES from subtitle_settings import SubtitleSettingsWidget +from settings_store import ( + SETTINGS_FILE, + load_settings, + normalize_settings, + save_settings, +) log = logging.getLogger("LiveTranslate.Panel") -SETTINGS_FILE = Path(__file__).parent / "user_settings.json" - - def _load_saved_settings() -> dict | None: - try: - if SETTINGS_FILE.exists(): - data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) - migrate_funasr_settings(data) - log.info(f"Loaded saved settings from {SETTINGS_FILE}") - return data - except Exception as e: - log.warning(f"Failed to load settings: {e}") - return None + return load_settings() def _save_settings(settings: dict): - try: - tmp = SETTINGS_FILE.with_suffix(".tmp") - tmp.write_text( - json.dumps(settings, indent=2, ensure_ascii=False), encoding="utf-8" - ) - tmp.replace(SETTINGS_FILE) - log.info(f"Settings saved to {SETTINGS_FILE}") - except Exception as e: - log.warning(f"Failed to save settings: {e}") + save_settings(settings) class ControlPanel(QWidget): @@ -89,6 +74,7 @@ class ControlPanel(QWidget): model_changed = pyqtSignal(dict) models_list_changed = pyqtSignal(list, int) subtitle_settings_changed = pyqtSignal(dict) + asr_language_changed = pyqtSignal(str) _bench_result = pyqtSignal(str) _cache_result = pyqtSignal(list) reset_positions = pyqtSignal() @@ -101,106 +87,7 @@ def __init__(self, config, saved_settings=None): self.resize(520, 650) saved = migrate_funasr_settings(saved_settings) or _load_saved_settings() - if saved: - self._current_settings = saved - else: - tc = config["translation"] - self._current_settings = { - "vad_mode": "silero", - "vad_threshold": config["asr"]["vad_threshold"], - "energy_threshold": 0.02, - "min_speech_duration": config["asr"]["min_speech_duration"], - "max_speech_duration": config["asr"]["max_speech_duration"], - "silence_mode": "auto", - "silence_duration": 0.8, - "asr_language": config["asr"].get("language", "auto"), - "asr_engine": "funasr", - "funasr_model": config["asr"].get( - "funasr_model", DEFAULT_FUNASR_MODEL - ), - "crispasr_model": config["asr"].get("crispasr_model", ""), - "crispasr_backend": config["asr"].get("crispasr_backend", "auto"), - "crispasr_gpu_backend": config["asr"].get( - "crispasr_gpu_backend", "auto" - ), - "crispasr_device_index": config["asr"].get( - "crispasr_device_index", 0 - ), - "crispasr_punc_model": config["asr"].get( - "crispasr_punc_model", "auto" - ), - "crispasr_unified_memory": config["asr"].get( - "crispasr_unified_memory", True - ), - "asr_device": "cuda", - "sensevoice_pad_seconds": config["asr"].get( - "sensevoice_pad_seconds", 0.5 - ), - "whisper_pad_seconds": config["asr"].get( - "whisper_pad_seconds", 0.5 - ), - "models": [ - { - "name": f"{tc['model']}", - "api_base": tc["api_base"], - "api_key": tc["api_key"], - "model": tc["model"], - } - ], - "active_model": 0, - "hub": "ms", - } - - if "models" not in self._current_settings: - tc = config["translation"] - self._current_settings["models"] = [ - { - "name": f"{tc['model']}", - "api_base": tc["api_base"], - "api_key": tc["api_key"], - "model": tc["model"], - } - ] - self._current_settings["active_model"] = 0 - - self._current_settings.setdefault( - "funasr_model", - config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL), - ) - self._current_settings["funasr_model"] = normalize_funasr_model_key( - self._current_settings.get("funasr_model") - ) - self._current_settings.setdefault( - "sensevoice_pad_seconds", - config["asr"].get("sensevoice_pad_seconds", 0.5), - ) - self._current_settings.setdefault( - "whisper_pad_seconds", - config["asr"].get("whisper_pad_seconds", 0.5), - ) - self._current_settings.setdefault( - "crispasr_model", - config["asr"].get("crispasr_model", ""), - ) - self._current_settings.setdefault( - "crispasr_backend", config["asr"].get("crispasr_backend", "auto") - ) - self._current_settings.setdefault( - "crispasr_gpu_backend", - config["asr"].get("crispasr_gpu_backend", "auto"), - ) - self._current_settings.setdefault( - "crispasr_device_index", - config["asr"].get("crispasr_device_index", 0), - ) - self._current_settings.setdefault( - "crispasr_punc_model", - config["asr"].get("crispasr_punc_model", "auto"), - ) - self._current_settings.setdefault( - "crispasr_unified_memory", - config["asr"].get("crispasr_unified_memory", True), - ) + self._current_settings = normalize_settings(config, saved) layout = QVBoxLayout(self) tabs = QTabWidget() @@ -272,6 +159,9 @@ def _create_vad_tab(self): asr_layout.addWidget(QLabel(t("label_language_hint")), 1, 0) asr_layout.addWidget(self._asr_lang, 1, 1) self._asr_lang.currentIndexChanged.connect(self._auto_save) + self._asr_lang.currentIndexChanged.connect( + lambda _idx: self.asr_language_changed.emit(self._get_asr_lang_code()) + ) self._asr_device = QComboBox() devices = ["cuda", "cpu"] @@ -1472,6 +1362,59 @@ def _refresh_model_list(self): item.setFont(font) self._model_list.addItem(item) + def refresh_model_list(self): + self._refresh_model_list() + + def current_settings(self) -> dict: + return dict(self._current_settings) + + def set_active_model(self, index: int, save: bool = True, emit: bool = True) -> bool: + models = self._current_settings.get("models", []) + if not (0 <= index < len(models)): + return False + self._current_settings["active_model"] = index + self._refresh_model_list() + if save: + _save_settings(self._current_settings) + self._emit_models_list_changed() + if emit: + self.model_changed.emit(models[index]) + return True + + def set_target_language(self, code: str, save: bool = True): + self._current_settings["target_language"] = code + if save: + _save_settings(self._current_settings) + + def set_asr_language(self, code: str, save: bool = True, emit: bool = False): + self._current_settings["asr_language"] = code + idx = self._asr_lang.findData(code) + if idx >= 0: + self._asr_lang.blockSignals(True) + self._asr_lang.setCurrentIndex(idx) + self._asr_lang.blockSignals(False) + if save: + _save_settings(self._current_settings) + if emit: + self.asr_language_changed.emit(code) + + def current_asr_language(self) -> str: + return self._get_asr_lang_code() + + def update_subtitle_mode(self, patch: dict, save: bool = True) -> dict: + mode = dict(self._current_settings.get("subtitle_mode") or {}) + mode.update(patch) + self._current_settings["subtitle_mode"] = mode + if save: + _save_settings(self._current_settings) + return mode + + def update_settings(self, patch: dict, save: bool = True) -> dict: + self._current_settings.update(patch) + if save: + _save_settings(self._current_settings) + return dict(self._current_settings) + def _emit_models_list_changed(self): models = self._current_settings.get("models", []) active_idx = self._current_settings.get("active_model", 0) @@ -1732,7 +1675,7 @@ def _apply_settings(self): self.settings_changed.emit(dict(self._current_settings)) def get_settings(self): - return dict(self._current_settings) + return self.current_settings() def get_active_model(self) -> dict | None: models = self._current_settings.get("models", []) diff --git a/main.py b/main.py index bbd870d..1c54ab4 100644 --- a/main.py +++ b/main.py @@ -67,12 +67,8 @@ from subtitle_overlay import SubtitleOverlay from subtitle_window import SubtitleWindow from log_window import LogWindow -from control_panel import ( - ControlPanel, - SETTINGS_FILE, - _load_saved_settings, - _save_settings, -) +from control_panel import ControlPanel +from settings_store import SETTINGS_FILE, load_settings, save_settings from dialogs import ( SetupWizardDialog, ModelDownloadDialog, @@ -295,11 +291,11 @@ def _on_settings_changed(self, settings): ) ) if "audio_device" in settings: - old_device = self._audio._device_name + old_device = self._audio.device_name self._audio.set_device(settings["audio_device"]) if old_device != settings.get("audio_device"): self._vad.flush() - self._vad._reset() + self._vad.reset() if self._overlay: self._overlay.update_monitor(0.0, 0.0) if "mic_device" in settings: @@ -397,11 +393,7 @@ def _on_target_language_changed(self, lang: str): if self._translator: self._translator.set_target_language(lang) if self._panel: - settings = self._panel.get_settings() - settings["target_language"] = lang - from control_panel import _save_settings - - _save_settings(settings) + self._panel.set_target_language(lang) def _on_model_changed(self, model_config: dict): log.info( @@ -568,7 +560,7 @@ def _switch_asr_engine(self, engine_type: str): self._last_interim_check_time = 0.0 self._interim_committed_tail = "" self._vad.flush() - self._vad._reset() + self._vad.reset() cached = is_asr_cached(engine_type, cache_model_key, hub) display_name = ASR_DISPLAY_NAMES.get(engine_type, engine_type) @@ -826,7 +818,7 @@ def _mem_snapshot(self) -> dict: except Exception: pass msgs = len(self._overlay._messages) if self._overlay else 0 - vad_buf = len(self._vad._speech_buffer) + vad_buf = self._vad.buffer_stats()["chunks"] return { "rss": rss_mb, "gpu_alloc": gpu_alloc_mb, @@ -1477,8 +1469,8 @@ def _capture_loop(self): while self._running: item = self._audio.get_audio(timeout=1.0) if item is None: - if self._vad._is_speaking and not self._paused: - n = self._vad._get_effective_silence_limit() + 1 + if self._vad.is_speaking and not self._paused: + n = self._vad.effective_silence_limit_chunks() + 1 for _ in range(n): with self._vad_lock: seg = self._vad.process_chunk(silence_chunk) @@ -1503,8 +1495,8 @@ def _capture_loop(self): if speech_segment is None: # Still accumulating — check for interim ASR if (self._incremental_enabled and self._asr_ready - and self._vad._is_speaking): - buf_samples = self._vad._speech_samples + and self._vad.is_speaking): + buf_samples = self._vad.speech_samples total_dur = buf_samples / 16000 elapsed = (buf_samples - self._last_interim_samples) / 16000 now = time.perf_counter() @@ -1560,7 +1552,7 @@ def _asr_loop(self): self._drain_interim_duplicates() self._do_interim_asr() with self._vad_lock: - self._last_interim_samples = self._vad._speech_samples + self._last_interim_samples = self._vad.speech_samples def _drain_interim_duplicates(self): while True: @@ -1580,7 +1572,7 @@ def main(): config.setdefault("asr", {}) config["asr"].setdefault("asr_engine", "funasr") config["asr"].setdefault("funasr_model", DEFAULT_FUNASR_MODEL) - saved = _load_saved_settings() + saved = load_settings(config) migrate_funasr_settings(saved) # Log actual effective config @@ -1624,7 +1616,7 @@ def main(): wizard = SetupWizardDialog() if wizard.exec() != QDialog.DialogCode.Accepted: sys.exit(0) - saved = _load_saved_settings() + saved = load_settings(config) log.info("Setup wizard completed") # Prompt user to configure translation API @@ -1640,7 +1632,7 @@ def main(): dlg = ModelEditDialog(None, { "name": "hunyuan-mt-chimera-7b", "api_base": "http://127.0.0.1:1234/v1", - "api_key": "sk-lm-tHzDfNGm:dgxlip7eebn3HIMxivqN", + "api_key": "", "model": "hunyuan-mt-chimera-7b", }) dlg.setWindowTitle(t("setup_api_title")) @@ -1649,7 +1641,7 @@ def main(): if data.get("api_key"): saved["models"] = [data] saved["active_model"] = 0 - _save_settings(saved) + save_settings(saved) log.info(f"Translation API configured: {data['name']}") # If user skips, ControlPanel will create default placeholder from config.yaml @@ -1803,33 +1795,26 @@ def on_toggle_overlay(): # --- Subtitle window toggle --- def _save_overlay_pos(): - settings = panel.get_settings() pos = overlay.pos() size = overlay.size() - settings["overlay_x"] = pos.x() - settings["overlay_y"] = pos.y() - settings["overlay_w"] = size.width() - settings["overlay_h"] = size.height() - panel._current_settings.update({ - "overlay_x": pos.x(), "overlay_y": pos.y(), - "overlay_w": size.width(), "overlay_h": size.height(), + panel.update_settings({ + "overlay_x": pos.x(), + "overlay_y": pos.y(), + "overlay_w": size.width(), + "overlay_h": size.height(), }) - _save_settings(settings) overlay.position_changed.connect(_save_overlay_pos) subwin_toggle_action = QAction(t("subwin_show"), checkable=True) def _save_subwin_state(): - settings = panel.get_settings() - sm = settings.get("subtitle_mode") or {} - sm["enabled"] = subwin.isVisible() pos = subwin.pos() - sm["window_x"] = pos.x() - sm["window_y"] = pos.y() - settings["subtitle_mode"] = sm - panel._current_settings["subtitle_mode"] = sm - _save_settings(settings) + panel.update_subtitle_mode({ + "enabled": subwin.isVisible(), + "window_x": pos.x(), + "window_y": pos.y(), + }) _subwin_notified = [False] @@ -1929,24 +1914,16 @@ def on_toggle_panel(): taskbar_action = QAction(t("taskbar"), checkable=True) # Tray → overlay sync - ct_action.toggled.connect(lambda v: overlay._handle._ct_check.setChecked(v)) - topmost_action.toggled.connect( - lambda v: overlay._handle._topmost_check.setChecked(v) - ) - autoscroll_action.toggled.connect( - lambda v: overlay._handle._auto_scroll.setChecked(v) - ) - taskbar_action.toggled.connect( - lambda v: overlay._handle._taskbar_check.setChecked(v) - ) + ct_action.toggled.connect(overlay.set_click_through_checked) + topmost_action.toggled.connect(overlay.set_topmost_checked) + autoscroll_action.toggled.connect(overlay.set_auto_scroll_checked) + taskbar_action.toggled.connect(overlay.set_taskbar_checked) # Overlay → tray sync - overlay._handle.click_through_toggled.connect(lambda v: ct_action.setChecked(v)) - overlay._handle.topmost_toggled.connect(lambda v: topmost_action.setChecked(v)) - overlay._handle.auto_scroll_toggled.connect( - lambda v: autoscroll_action.setChecked(v) - ) - overlay._handle.taskbar_toggled.connect(lambda v: taskbar_action.setChecked(v)) + overlay.click_through_toggled.connect(lambda v: ct_action.setChecked(v)) + overlay.topmost_toggled.connect(lambda v: topmost_action.setChecked(v)) + overlay.auto_scroll_toggled.connect(lambda v: autoscroll_action.setChecked(v)) + overlay.taskbar_toggled.connect(lambda v: taskbar_action.setChecked(v)) overlay_menu.addAction(ct_action) overlay_menu.addAction(topmost_action) @@ -1978,27 +1955,12 @@ def _rebuild_model_menu(): def _on_tray_model_switch(index): models = panel.get_settings().get("models", []) if 0 <= index < len(models): - from control_panel import _save_settings - - settings = panel.get_settings() - settings["active_model"] = index - panel._current_settings["active_model"] = index - _save_settings(settings) - panel._refresh_model_list() - live_trans._on_model_changed(models[index]) - overlay.set_models(models, index) + panel.set_active_model(index) def on_overlay_model_switch(index): models = panel.get_settings().get("models", []) if 0 <= index < len(models): - from control_panel import _save_settings - - settings = panel.get_settings() - settings["active_model"] = index - panel._current_settings["active_model"] = index - _save_settings(settings) - panel._refresh_model_list() - live_trans._on_model_changed(models[index]) + panel.set_active_model(index) _rebuild_model_menu() model_menu.aboutToShow.connect(_rebuild_model_menu) @@ -2032,12 +1994,6 @@ def on_overlay_model_switch(index): def _on_tray_lang_switch(lang_code): overlay.set_target_language(lang_code) live_trans._on_target_language_changed(lang_code) - from control_panel import _save_settings - - settings = panel.get_settings() - settings["target_language"] = lang_code - panel._current_settings["target_language"] = lang_code - _save_settings(settings) # Overlay → tray lang sync def _on_overlay_lang_changed(lang_code): @@ -2073,19 +2029,11 @@ def _on_overlay_lang_changed(lang_code): _asr_lang_actions[current_asr_lang].setChecked(True) def _on_tray_asr_lang(code): - from control_panel import _save_settings - live_trans._set_asr_language(code) - settings = panel.get_settings() - settings["asr_language"] = code - panel._current_settings["asr_language"] = code - _save_settings(settings) - # Sync control panel combo - idx = panel._asr_lang.findData(code) - if idx >= 0: - panel._asr_lang.blockSignals(True) - panel._asr_lang.setCurrentIndex(idx) - panel._asr_lang.blockSignals(False) + panel.set_asr_language(code) + overlay.set_source_language(code) + if code in _asr_lang_actions: + _asr_lang_actions[code].setChecked(True) menu.addMenu(asr_lang_menu) menu.addSeparator() @@ -2121,15 +2069,16 @@ def on_quit(): def _on_overlay_source_lang(code): """Overlay source language combo → sync to panel + ASR engine + tray.""" _on_tray_asr_lang(code) - overlay.set_source_language(code) - def _on_panel_asr_lang_changed(_index): - """Panel ASR language combo → sync to overlay.""" - code = panel._asr_lang.currentData() or "auto" + def _on_panel_asr_lang_changed(code): + """Panel ASR language combo → sync to runtime, overlay, and tray.""" + live_trans._set_asr_language(code) overlay.set_source_language(code) + if code in _asr_lang_actions: + _asr_lang_actions[code].setChecked(True) overlay.source_language_changed.connect(_on_overlay_source_lang) - panel._asr_lang.currentIndexChanged.connect(_on_panel_asr_lang_changed) + panel.asr_language_changed.connect(_on_panel_asr_lang_changed) overlay.model_switch_requested.connect(on_overlay_model_switch) overlay.start_requested.connect(on_resume) overlay.stop_requested.connect(on_pause) diff --git a/settings_store.py b/settings_store.py new file mode 100644 index 0000000..5623f96 --- /dev/null +++ b/settings_store.py @@ -0,0 +1,137 @@ +import json +import logging +from pathlib import Path +from typing import Any + +from model_manager import ( + DEFAULT_FUNASR_MODEL, + migrate_funasr_settings, + normalize_funasr_model_key, +) + +log = logging.getLogger("LiveTranslate.Settings") + +SETTINGS_FILE = Path(__file__).parent / "user_settings.json" + + +def normalize_settings(config: dict[str, Any], saved: dict | None = None) -> dict: + """Return a complete settings dict with migrations and defaults applied.""" + settings = dict(saved or {}) + migrate_funasr_settings(settings) + + asr = config.get("asr", {}) + translation = config.get("translation", {}) + + settings.setdefault("vad_mode", "silero") + settings.setdefault("vad_threshold", asr.get("vad_threshold", 0.5)) + settings.setdefault("energy_threshold", 0.02) + settings.setdefault("min_speech_duration", asr.get("min_speech_duration", 1.0)) + settings.setdefault("max_speech_duration", asr.get("max_speech_duration", 8.0)) + settings.setdefault("silence_mode", "auto") + settings.setdefault("silence_duration", 0.8) + settings.setdefault("incremental_asr", False) + settings.setdefault("interim_interval", 2.0) + + settings.setdefault("asr_language", asr.get("language", "auto")) + settings.setdefault("asr_engine", asr.get("asr_engine", "funasr")) + settings.setdefault("funasr_model", asr.get("funasr_model", DEFAULT_FUNASR_MODEL)) + settings["funasr_model"] = normalize_funasr_model_key( + settings.get("funasr_model") + ) + settings.setdefault("whisper_model_size", asr.get("model_size", "medium")) + settings.setdefault("crispasr_model", asr.get("crispasr_model", "")) + settings.setdefault("crispasr_backend", asr.get("crispasr_backend", "auto")) + settings.setdefault( + "crispasr_gpu_backend", asr.get("crispasr_gpu_backend", "auto") + ) + settings.setdefault("crispasr_device_index", asr.get("crispasr_device_index", 0)) + settings.setdefault("crispasr_punc_model", asr.get("crispasr_punc_model", "auto")) + settings.setdefault( + "crispasr_unified_memory", asr.get("crispasr_unified_memory", True) + ) + settings.setdefault("asr_device", asr.get("device", "cuda")) + settings.setdefault( + "sensevoice_pad_seconds", asr.get("sensevoice_pad_seconds", 0.5) + ) + settings.setdefault("whisper_pad_seconds", asr.get("whisper_pad_seconds", 0.5)) + settings.setdefault("audio_device", config.get("audio", {}).get("device")) + settings.setdefault("mic_device", None) + settings.setdefault("hub", "ms") + + if "models" not in settings: + model = translation.get("model", "") + settings["models"] = [ + { + "name": model or "Local API", + "api_base": translation.get("api_base", ""), + "api_key": translation.get("api_key", ""), + "model": model, + } + ] + settings.setdefault("active_model", 0) + models = settings.get("models") or [] + if not isinstance(models, list): + models = [] + settings["models"] = models + if models: + active = settings.get("active_model", 0) + if not isinstance(active, int) or active < 0 or active >= len(models): + settings["active_model"] = 0 + + settings.setdefault("target_language", translation.get("target_language", "zh")) + settings.setdefault("source_language", translation.get("source_language", "auto")) + settings.setdefault("context_window", translation.get("context_window", 0)) + settings.setdefault("system_prompt", translation.get("system_prompt", "")) + settings.setdefault("timeout", 5) + settings.setdefault("auto_save_transcript", True) + + return settings + + +def load_settings(config: dict | None = None) -> dict | None: + try: + if SETTINGS_FILE.exists(): + data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) + migrate_funasr_settings(data) + if config is not None: + data = normalize_settings(config, data) + log.info(f"Loaded saved settings from {SETTINGS_FILE}") + return data + except Exception as e: + log.warning(f"Failed to load settings: {e}") + return None + + +def save_settings(settings: dict): + try: + tmp = SETTINGS_FILE.with_suffix(".tmp") + tmp.write_text( + json.dumps(settings, indent=2, ensure_ascii=False), encoding="utf-8" + ) + tmp.replace(SETTINGS_FILE) + log.info(f"Settings saved to {SETTINGS_FILE}") + except Exception as e: + log.warning(f"Failed to save settings: {e}") + + +def update_settings(patch: dict, config: dict | None = None) -> dict: + settings = load_settings(config) or {} + settings.update(patch) + if config is not None: + settings = normalize_settings(config, settings) + save_settings(settings) + return settings + + +class SettingsRepository: + def __init__(self, config: dict): + self._config = config + + def load(self) -> dict | None: + return load_settings(self._config) + + def save(self, settings: dict): + save_settings(settings) + + def update(self, patch: dict) -> dict: + return update_settings(patch, self._config) diff --git a/subtitle_overlay.py b/subtitle_overlay.py index 064490d..7887734 100644 --- a/subtitle_overlay.py +++ b/subtitle_overlay.py @@ -831,6 +831,18 @@ def set_source_language(self, lang: str): self._source_lang.setCurrentIndex(idx) self._source_lang.blockSignals(False) + def set_click_through_checked(self, value: bool): + self._ct_check.setChecked(value) + + def set_topmost_checked(self, value: bool): + self._topmost_check.setChecked(value) + + def set_auto_scroll_checked(self, value: bool): + self._auto_scroll.setChecked(value) + + def set_taskbar_checked(self, value: bool): + self._taskbar_check.setChecked(value) + def set_models(self, models: list, active_index: int = 0): self._model_combo.blockSignals(True) self._model_combo.clear() @@ -895,6 +907,10 @@ class SubtitleOverlay(QWidget): target_language_changed = pyqtSignal(str) source_language_changed = pyqtSignal(str) model_switch_requested = pyqtSignal(int) + click_through_toggled = pyqtSignal(bool) + topmost_toggled = pyqtSignal(bool) + auto_scroll_toggled = pyqtSignal(bool) + taskbar_toggled = pyqtSignal(bool) start_requested = pyqtSignal() stop_requested = pyqtSignal() hide_requested = pyqtSignal() @@ -965,6 +981,10 @@ def _setup_ui(self): self._handle.click_through_toggled.connect(self._set_click_through) self._handle.topmost_toggled.connect(self._set_topmost) self._handle.taskbar_toggled.connect(self._set_taskbar) + self._handle.click_through_toggled.connect(self.click_through_toggled.emit) + self._handle.topmost_toggled.connect(self.topmost_toggled.emit) + self._handle.auto_scroll_toggled.connect(self.auto_scroll_toggled.emit) + self._handle.taskbar_toggled.connect(self.taskbar_toggled.emit) self._handle.target_language_changed.connect(self.target_language_changed.emit) self._handle.source_language_changed.connect(self.source_language_changed.emit) self._handle.model_changed.connect(self.model_switch_requested.emit) @@ -1043,6 +1063,18 @@ def resizeEvent(self, event): def set_running(self, running: bool): self._handle.set_running(running) + def set_click_through_checked(self, value: bool): + self._handle.set_click_through_checked(value) + + def set_topmost_checked(self, value: bool): + self._handle.set_topmost_checked(value) + + def set_auto_scroll_checked(self, value: bool): + self._handle.set_auto_scroll_checked(value) + + def set_taskbar_checked(self, value: bool): + self._handle.set_taskbar_checked(value) + def _set_topmost(self, enabled: bool): flags = self.windowFlags() if enabled: diff --git a/vad_processor.py b/vad_processor.py index b0f23ed..a72e068 100644 --- a/vad_processor.py +++ b/vad_processor.py @@ -76,6 +76,28 @@ def __init__( # Exposed for monitor self.last_confidence = 0.0 + @property + def is_speaking(self) -> bool: + return self._is_speaking + + @property + def speech_samples(self) -> int: + return self._speech_samples + + def reset(self): + self._reset() + + def effective_silence_limit_chunks(self) -> int: + return self._get_effective_silence_limit() + + def buffer_stats(self) -> dict: + return { + "chunks": len(self._speech_buffer), + "samples": self._speech_samples, + "seconds": self._speech_samples / self.sample_rate, + "is_speaking": self._is_speaking, + } + def _seconds_to_chunks(self, seconds: float) -> int: return max(1, round(seconds / self._chunk_duration)) From 2788464119ac2084802e40a0d1271c48c7c82917 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 05:34:46 +0800 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=E6=8A=BD=E5=87=BA=20ASRService?= =?UTF-8?q?=20=E7=AE=A1=E7=90=86=20worker=20=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 asr_service.py,集中 ASR worker 配置构造、缺失模型判断、下载后校验、加载切换和失败恢复逻辑。 LiveTranslateApp 不再直接持有 ASRClient,仅负责触发下载/加载对话框、重置增量 ASR 状态和更新 overlay 状态显示。 转写、语言切换、padding 更新、ready 状态和 shutdown 统一通过 ASRService 公开接口访问,为后续 PipelineController 拆分降低 main.py 的运行时耦合。 --- asr_service.py | 579 +++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 500 ++++++------------------------------------ 2 files changed, 645 insertions(+), 434 deletions(-) create mode 100644 asr_service.py diff --git a/asr_service.py b/asr_service.py new file mode 100644 index 0000000..07f915e --- /dev/null +++ b/asr_service.py @@ -0,0 +1,579 @@ +import logging +import threading +from dataclasses import dataclass +from pathlib import Path +from typing import Callable + +from asr_client import ASRClient, ASRWorkerError, ASRWorkerExited, ASRWorkerTimeout +from model_manager import ( + ASR_DISPLAY_NAMES, + CRISPASR_MODEL_PROFILES, + DEFAULT_FUNASR_MODEL, + MODELS_DIR, + crispasr_profile, + funasr_display_name, + funasr_supports_padding, + get_local_model_path, + get_missing_models, + is_asr_cached, + local_crispasr_display_name, + local_faster_whisper_display_name, + normalize_asr_engine_selection, + normalize_funasr_model_key, + resolve_custom_crispasr_model, + resolve_custom_whisper_model, +) + +log = logging.getLogger("LiveTranslate.ASRService") + + +@dataclass +class ASRStatus: + ready: bool + engine_type: str | None + device: str | None + display_name: str | None + worker_status: str | None + + +@dataclass +class ASRSwitchPlan: + engine_type: str + device: str + hub: str + download_proxy: str + display_name: str + cache_model_key: str + worker_config: dict + target_state: dict + missing_models: list[dict] + already_current: bool = False + error: str | None = None + + +@dataclass +class ASRSwitchResult: + status: str + target_state: dict | None = None + restored_state: dict | None = None + load_error: str | None = None + restore_error: str | None = None + + +class ASRService: + """Owns ASR worker lifecycle and exposes a small runtime API.""" + + def __init__( + self, + config: dict, + release_memory_caches: Callable[[], None] | None = None, + unavailable_callback: Callable[[str], None] | None = None, + ): + self._config = config + self._release_memory_caches = release_memory_caches + self._unavailable_callback = unavailable_callback + + self._asr_ready = False + self._asr_type = None + self._asr: ASRClient | None = None + self._asr_signature = None + self._asr_config = None + self._asr_error_count = 0 + self._asr_device = config["asr"]["device"] + self._whisper_model_size = config["asr"]["model_size"] + self._funasr_model_key = normalize_funasr_model_key( + config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL) + ) + self._crispasr_model_key = str(config["asr"].get("crispasr_model", "") or "") + self._asr_lock = threading.RLock() + + @property + def is_ready(self) -> bool: + with self._asr_lock: + return ( + self._asr_ready + and self._asr is not None + and self._asr.status == "ready" + ) + + @property + def current_engine_type(self) -> str | None: + with self._asr_lock: + return self._asr_type + + def status_snapshot(self) -> ASRStatus: + with self._asr_lock: + display_name = (self._asr_config or {}).get("display_name") + worker_status = self._asr.status if self._asr is not None else None + return ASRStatus( + ready=self.is_ready, + engine_type=self._asr_type, + device=self._asr_device, + display_name=display_name, + worker_status=worker_status, + ) + + def start_or_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: + """Build a switch plan. UI code may handle missing downloads before loading.""" + return self.prepare_switch(engine_type, settings) + + def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: + engine_type, funasr_model = normalize_asr_engine_selection( + engine_type, settings.get("funasr_model", self._funasr_model_key) + ) + device = settings.get("asr_device", self._asr_device) + hub = settings.get("hub", "ms") + download_proxy = settings.get("download_proxy", "system") + + model_size = settings.get( + "whisper_model_size", self._config["asr"]["model_size"] + ) + model_path = None + cache_model_key = model_size + crispasr_model = settings.get( + "crispasr_model", + self._config["asr"].get("crispasr_model", self._crispasr_model_key), + ) + crispasr_model = str(crispasr_model or "") + crispasr_model_path_value = None + crispasr_backend = settings.get( + "crispasr_backend", self._config["asr"].get("crispasr_backend", "auto") + ) + crispasr_gpu_backend = settings.get( + "crispasr_gpu_backend", + self._config["asr"].get("crispasr_gpu_backend", "auto"), + ) + crispasr_device_index = int( + settings.get( + "crispasr_device_index", + self._config["asr"].get("crispasr_device_index", 0), + ) + or 0 + ) + crispasr_punc_model = settings.get( + "crispasr_punc_model", + self._config["asr"].get("crispasr_punc_model", "auto"), + ) + crispasr_unified_memory = bool( + settings.get( + "crispasr_unified_memory", + self._config["asr"].get("crispasr_unified_memory", True), + ) + ) + + if engine_type == "whisper": + model_path = resolve_custom_whisper_model(model_size) + if model_path: + cache_model_key = model_path + elif engine_type == "funasr": + cache_model_key = funasr_model + elif engine_type == "crispasr": + if not crispasr_model: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + "CrispASR model is not selected; keeping current ASR worker", + ) + custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) + if custom_crispasr_path: + cache_model_key = custom_crispasr_path + crispasr_model_path_value = custom_crispasr_path + elif crispasr_model in CRISPASR_MODEL_PROFILES: + cache_model_key = crispasr_model + crispasr_model_path_value = get_local_model_path( + "crispasr", hub=hub, funasr_model=crispasr_model + ) + else: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"CrispASR model is invalid or unavailable: {crispasr_model}; " + "keeping current ASR worker", + ) + + compute = self._config["asr"]["compute_type"] + if engine_type == "whisper": + signature_model = cache_model_key + elif engine_type == "funasr": + signature_model = funasr_model + elif engine_type == "crispasr": + signature_model = ( + cache_model_key, + crispasr_backend, + crispasr_gpu_backend, + crispasr_device_index, + crispasr_punc_model, + crispasr_unified_memory, + ) + else: + signature_model = engine_type + language = settings.get( + "asr_language", self._config["asr"].get("language", "auto") + ) + signature = (engine_type, signature_model, device, hub, compute, language) + + with self._asr_lock: + current_asr = self._asr + current_ready = ( + self._asr_ready + and current_asr is not None + and current_asr.status == "ready" + ) + if current_ready and self._asr_signature == signature: + return ASRSwitchPlan( + engine_type=engine_type, + device=device, + hub=hub, + download_proxy=download_proxy, + display_name=(self._asr_config or {}).get("display_name") + or engine_type, + cache_model_key=str(cache_model_key), + worker_config={}, + target_state={}, + missing_models=[], + already_current=True, + ) + if not current_ready: + self._asr_ready = False + current_type = self._asr_type + + log.info(f"Switching ASR worker: {current_type} -> {engine_type}") + + cached = is_asr_cached(engine_type, cache_model_key, hub) + display_name = self._display_name( + engine_type, model_size, model_path, funasr_model, crispasr_model + ) + if engine_type == "crispasr": + custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) + profile = {} if custom_crispasr_path else crispasr_profile(crispasr_model) + if ( + profile.get("needs_punctuation") + and crispasr_punc_model in (None, "", "auto") + ): + crispasr_punc_model = "auto" + + worker_config = { + "engine_type": engine_type, + "funasr_model": funasr_model, + "model_size": cache_model_key, + "device": device, + "compute_type": compute, + "hub": hub, + "language": language, + "pad_seconds": ( + settings.get( + "sensevoice_pad_seconds", + self._config["asr"].get("sensevoice_pad_seconds", 0.5), + ) + if engine_type == "funasr" + else settings.get( + "whisper_pad_seconds", + self._config["asr"].get("whisper_pad_seconds", 0.5), + ) + if engine_type == "whisper" + else None + ), + "download_root": str((MODELS_DIR / "huggingface" / "hub").resolve()), + "display_name": display_name, + } + if engine_type == "crispasr": + worker_config.update( + { + "crispasr_model": crispasr_model, + "crispasr_model_path": crispasr_model_path_value, + "crispasr_backend": crispasr_backend, + "crispasr_gpu_backend": crispasr_gpu_backend, + "crispasr_device_index": crispasr_device_index, + "crispasr_punc_model": crispasr_punc_model, + "crispasr_unified_memory": crispasr_unified_memory, + } + ) + target_state = { + "type": engine_type, + "signature": signature, + "device": device, + "funasr_model_key": funasr_model + if engine_type == "funasr" + else self._funasr_model_key, + "whisper_model_size": model_size + if engine_type == "whisper" + else self._whisper_model_size, + "crispasr_model_key": crispasr_model + if engine_type == "crispasr" + else self._crispasr_model_key, + "config": worker_config, + "display_name": display_name, + } + + missing = [] + if not cached: + missing = get_missing_models(engine_type, cache_model_key, hub) + missing = [m for m in missing if m["type"] != "silero-vad"] + + return ASRSwitchPlan( + engine_type=engine_type, + device=device, + hub=hub, + download_proxy=download_proxy, + display_name=display_name, + cache_model_key=str(cache_model_key), + worker_config=worker_config, + target_state=target_state, + missing_models=missing, + ) + + def complete_download(self, plan: ASRSwitchPlan) -> str | None: + if plan.engine_type != "crispasr": + return None + model_path = get_local_model_path( + "crispasr", hub=plan.hub, funasr_model=plan.cache_model_key + ) + if not model_path: + return "CrispASR model file was not found after download" + plan.worker_config["crispasr_model_path"] = model_path + plan.target_state["config"] = plan.worker_config + return None + + def mark_download_cancelled(self): + with self._asr_lock: + self._asr_ready = self._asr is not None and self._asr.status == "ready" + + def switch_worker(self, plan: ASRSwitchPlan) -> ASRSwitchResult: + with self._asr_lock: + old_asr = self._asr + old_config = dict(self._asr_config) if self._asr_config else None + old_state = { + "type": self._asr_type, + "signature": self._asr_signature, + "device": self._asr_device, + "funasr_model_key": self._funasr_model_key, + "whisper_model_size": self._whisper_model_size, + "crispasr_model_key": self._crispasr_model_key, + "config": old_config, + "display_name": (old_config or {}).get("display_name"), + } + self._asr = None + self._asr_ready = False + self._asr_type = None + self._asr_signature = None + self._asr_config = None + self._asr_error_count = 0 + + new_asr = None + restored_asr = None + load_error = None + restore_error = None + + if old_asr is not None: + log.info(f"Stopping old ASR worker before switch: pid={old_asr.pid}") + old_asr.shutdown() + if self._release_memory_caches is not None: + self._release_memory_caches() + + try: + new_asr = self._load_asr_client(plan.worker_config) + except Exception as exc: + load_error = str(exc) + log.error(f"Failed to load ASR worker: {exc}", exc_info=True) + if old_config: + try: + log.info("Restoring previous ASR worker after switch failure") + restored_asr = self._load_asr_client(old_config) + except Exception as restore_exc: + restore_error = str(restore_exc) + log.error( + f"Failed to restore previous ASR worker: {restore_exc}", + exc_info=True, + ) + + if new_asr is not None: + self._activate_asr(new_asr, plan.target_state) + log.info(f"ASR worker ready: {plan.engine_type} on {plan.device}") + return ASRSwitchResult(status="ready", target_state=plan.target_state) + + if restored_asr is not None: + self._activate_asr(restored_asr, old_state) + log.info( + f"Previous ASR worker restored: " + f"{old_state.get('type')} on {old_state.get('device')}" + ) + return ASRSwitchResult( + status="restored", + restored_state=old_state, + load_error=load_error, + ) + + return ASRSwitchResult( + status="failed", load_error=load_error, restore_error=restore_error + ) + + def shutdown(self): + with self._asr_lock: + client = self._asr + self._asr = None + self._asr_ready = False + self._asr_type = None + self._asr_signature = None + self._asr_config = None + self._asr_error_count = 0 + if client is not None: + log.info(f"Shutting down ASR worker: pid={client.pid}") + client.shutdown() + + def transcribe(self, audio, **kwargs): + with self._asr_lock: + if not self._asr_ready or self._asr is None: + return None + client = self._asr + try: + result = client.transcribe(audio, **kwargs) + except (ASRWorkerExited, ASRWorkerTimeout) as exc: + self._mark_asr_unavailable(str(exc), client) + raise + except ASRWorkerError as exc: + self._asr_error_count += 1 + if not exc.recoverable or self._asr_error_count >= 3: + self._mark_asr_unavailable(str(exc), client) + raise + self._asr_error_count = 0 + return result + + def set_language(self, language: str): + with self._asr_lock: + client = self._asr + if not self._asr_ready or client is None: + return + try: + client.set_language(language) + except (ASRWorkerExited, ASRWorkerTimeout) as exc: + self._mark_asr_unavailable(str(exc), client) + except ASRWorkerError as exc: + log.warning(f"ASR language update failed: {exc}") + + def set_padding(self, engine_type: str, pad_seconds): + with self._asr_lock: + client = self._asr + if not self._asr_ready or client is None or self._asr_type != engine_type: + return + if engine_type == "funasr" and not funasr_supports_padding( + self._funasr_model_key + ): + return + try: + client.set_input_padding(pad_seconds) + except (ASRWorkerExited, ASRWorkerTimeout) as exc: + self._mark_asr_unavailable(str(exc), client) + except ASRWorkerError as exc: + log.warning(f"ASR padding update failed: {exc}") + + def _invalid_plan( + self, + engine_type: str, + device: str, + hub: str, + download_proxy: str, + message: str, + ) -> ASRSwitchPlan: + log.warning(message) + with self._asr_lock: + if not ( + self._asr_ready + and self._asr is not None + and self._asr.status == "ready" + ): + self._asr_ready = False + return ASRSwitchPlan( + engine_type=engine_type, + device=device, + hub=hub, + download_proxy=download_proxy, + display_name=engine_type, + cache_model_key="", + worker_config={}, + target_state={}, + missing_models=[], + already_current=True, + error=message, + ) + + def _mark_asr_unavailable(self, reason: str, client=None): + with self._asr_lock: + current = client or self._asr + if client is not None and self._asr is not client: + return + self._asr_ready = False + self._asr = None + self._asr_type = None + self._asr_signature = None + self._asr_config = None + self._asr_error_count = 0 + if current is not None: + try: + current.shutdown() + except Exception: + try: + current.terminate() + except Exception: + pass + log.warning(f"ASR worker unavailable: {reason}") + if self._unavailable_callback is not None: + self._unavailable_callback(reason) + + def _load_asr_client(self, worker_config: dict) -> ASRClient: + client = ASRClient(worker_config) + try: + client.start() + client.wait_ready() + return client + except Exception: + client.shutdown() + raise + + def _activate_asr(self, client: ASRClient, state: dict): + with self._asr_lock: + self._asr = client + self._asr_type = state["type"] + self._asr_signature = state["signature"] + self._asr_device = state["device"] + self._asr_config = dict(state["config"]) if state["config"] else None + self._funasr_model_key = state["funasr_model_key"] + self._whisper_model_size = state["whisper_model_size"] + self._crispasr_model_key = state["crispasr_model_key"] + self._asr_ready = True + self._asr_error_count = 0 + + @staticmethod + def _display_name( + engine_type: str, + model_size: str, + model_path: str | None, + funasr_model: str, + crispasr_model: str, + ) -> str: + display_name = ASR_DISPLAY_NAMES.get(engine_type, engine_type) + if engine_type == "whisper": + display_model = ( + local_faster_whisper_display_name(model_size) + if model_path + else model_size + ) or Path(model_size).name + display_name = f"Whisper {display_model}" + elif engine_type == "funasr": + display_name = funasr_display_name(funasr_model) + elif engine_type == "crispasr": + custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) + if custom_crispasr_path: + display_model = ( + local_crispasr_display_name(crispasr_model) + or Path(crispasr_model).name + ) + display_name = f"CrispASR {display_model}" + else: + display_name = ( + f"CrispASR {crispasr_profile(crispasr_model)['display_name']}" + ) + return display_name diff --git a/main.py b/main.py index 1c54ab4..2cd8300 100644 --- a/main.py +++ b/main.py @@ -17,24 +17,11 @@ from datetime import datetime from model_manager import ( - CRISPASR_MODEL_PROFILES, DEFAULT_FUNASR_MODEL, apply_cache_env, - crispasr_profile, - funasr_display_name, - funasr_supports_padding, get_missing_models, - get_local_model_path, - is_asr_cached, - ASR_DISPLAY_NAMES, - MODELS_DIR, - local_crispasr_display_name, - local_faster_whisper_display_name, migrate_funasr_settings, normalize_asr_engine_selection, - normalize_funasr_model_key, - resolve_custom_crispasr_model, - resolve_custom_whisper_model, ) # Set cache env BEFORE importing torch so TORCH_HOME is respected @@ -47,7 +34,7 @@ from audio_capture import AudioCapture from vad_processor import VADProcessor -from asr_client import ASRClient, ASRWorkerError, ASRWorkerExited, ASRWorkerTimeout +from asr_service import ASRService from translator import Translator, RepetitionError from transcript_writer import TranscriptWriter @@ -159,7 +146,6 @@ def __init__(self, config): self._config = config self._running = False self._paused = False - self._asr_ready = False # True when ASR model is loaded self._audio = AudioCapture( device=config["audio"].get("device"), @@ -173,18 +159,11 @@ def __init__(self, config): max_speech_duration=config["asr"]["max_speech_duration"], chunk_duration=config["audio"]["chunk_duration"], ) - self._asr_type = None - self._asr = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 - self._asr_device = config["asr"]["device"] - self._whisper_model_size = config["asr"]["model_size"] - self._funasr_model_key = normalize_funasr_model_key( - config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL) + self._asr_service = ASRService( + config, + release_memory_caches=self._release_memory_caches, + unavailable_callback=self._on_asr_unavailable, ) - self._crispasr_model_key = str(config["asr"].get("crispasr_model", "") or "") - self._asr_lock = threading.RLock() self._vad_lock = threading.Lock() self._target_language = config["translation"]["target_language"] self._translator = Translator( @@ -263,11 +242,11 @@ def _on_settings_changed(self, settings): if "style" in settings and self._overlay: self._overlay.apply_style(settings["style"]) if "asr_language" in settings: - self._set_asr_language(settings["asr_language"]) + self._asr_service.set_language(settings["asr_language"]) if "sensevoice_pad_seconds" in settings: - self._set_asr_padding("funasr", settings["sensevoice_pad_seconds"]) + self._asr_service.set_padding("funasr", settings["sensevoice_pad_seconds"]) if "whisper_pad_seconds" in settings: - self._set_asr_padding("whisper", settings["whisper_pad_seconds"]) + self._asr_service.set_padding("whisper", settings["whisper_pad_seconds"]) if any( key in settings for key in ( @@ -287,7 +266,8 @@ def _on_settings_changed(self, settings): self._switch_asr_engine( settings.get( "asr_engine", - self._asr_type or self._config["asr"].get("asr_engine", "funasr"), + self._asr_service.current_engine_type + or self._config["asr"].get("asr_engine", "funasr"), ) ) if "audio_device" in settings: @@ -313,80 +293,10 @@ def _on_settings_changed(self, settings): if "auto_save_transcript" in settings: self._transcript.set_enabled(settings["auto_save_transcript"]) - def _mark_asr_unavailable(self, reason: str, client=None): - with self._asr_lock: - current = client or self._asr - if client is not None and self._asr is not client: - return - self._asr_ready = False - self._asr = None - self._asr_type = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 - if current is not None: - try: - current.shutdown() - except Exception: - try: - current.terminate() - except Exception: - pass - log.warning(f"ASR worker unavailable: {reason}") + def _on_asr_unavailable(self, reason: str): if self._overlay: self._overlay.update_asr_device("ASR unavailable") - def _shutdown_asr_worker(self): - with self._asr_lock: - client = self._asr - self._asr = None - self._asr_ready = False - self._asr_type = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 - if client is not None: - log.info(f"Shutting down ASR worker: pid={client.pid}") - client.shutdown() - - def _set_asr_language(self, language: str): - with self._asr_lock: - client = self._asr - if not self._asr_ready or client is None: - return - try: - client.set_language(language) - except (ASRWorkerExited, ASRWorkerTimeout) as exc: - self._mark_asr_unavailable(str(exc), client) - except ASRWorkerError as exc: - log.warning(f"ASR language update failed: {exc}") - - def _set_asr_padding(self, engine_type: str, pad_seconds): - with self._asr_lock: - client = self._asr - if not self._asr_ready or client is None or self._asr_type != engine_type: - return - if engine_type == "funasr" and not funasr_supports_padding( - self._funasr_model_key - ): - return - try: - client.set_input_padding(pad_seconds) - except (ASRWorkerExited, ASRWorkerTimeout) as exc: - self._mark_asr_unavailable(str(exc), client) - except ASRWorkerError as exc: - log.warning(f"ASR padding update failed: {exc}") - - def _load_asr_client(self, worker_config: dict) -> ASRClient: - client = ASRClient(worker_config) - try: - client.start() - client.wait_ready() - return client - except Exception: - client.shutdown() - raise - def _on_target_language_changed(self, lang: str): self._target_language = lang log.info(f"Target language: {lang}") @@ -433,125 +343,10 @@ def _on_model_changed(self, model_config: dict): def _switch_asr_engine(self, engine_type: str): settings = self._panel.get_settings() if self._panel else {} - engine_type, funasr_model = normalize_asr_engine_selection( - engine_type, settings.get("funasr_model", self._funasr_model_key) - ) - device = settings.get("asr_device", self._asr_device) - hub = "ms" - download_proxy = "system" - if self._panel: - hub = settings.get("hub", "ms") - download_proxy = settings.get("download_proxy", "system") - - model_size = self._config["asr"]["model_size"] - if self._panel: - model_size = settings.get("whisper_model_size", model_size) - model_path = None - cache_model_key = model_size - crispasr_model = settings.get( - "crispasr_model", - self._config["asr"].get("crispasr_model", self._crispasr_model_key), - ) - crispasr_model = str(crispasr_model or "") - crispasr_model_path_value = None - crispasr_backend = settings.get( - "crispasr_backend", self._config["asr"].get("crispasr_backend", "auto") - ) - crispasr_gpu_backend = settings.get( - "crispasr_gpu_backend", - self._config["asr"].get("crispasr_gpu_backend", "auto"), - ) - crispasr_device_index = int( - settings.get( - "crispasr_device_index", - self._config["asr"].get("crispasr_device_index", 0), - ) - or 0 - ) - crispasr_punc_model = settings.get( - "crispasr_punc_model", - self._config["asr"].get("crispasr_punc_model", "auto"), - ) - crispasr_unified_memory = bool( - settings.get( - "crispasr_unified_memory", - self._config["asr"].get("crispasr_unified_memory", True), - ) - ) - if engine_type == "whisper": - model_path = resolve_custom_whisper_model(model_size) - if model_path: - cache_model_key = model_path - elif engine_type == "funasr": - cache_model_key = funasr_model - elif engine_type == "crispasr": - if not crispasr_model: - log.warning("CrispASR model is not selected; keeping current ASR worker") - with self._asr_lock: - if not ( - self._asr_ready - and self._asr is not None - and self._asr.status == "ready" - ): - self._asr_ready = False - return - custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) - if custom_crispasr_path: - cache_model_key = custom_crispasr_path - crispasr_model_path_value = custom_crispasr_path - elif crispasr_model in CRISPASR_MODEL_PROFILES: - cache_model_key = crispasr_model - crispasr_model_path_value = get_local_model_path( - "crispasr", hub=hub, funasr_model=crispasr_model - ) - else: - log.warning( - "CrispASR model is invalid or unavailable: %s; keeping current ASR worker", - crispasr_model, - ) - with self._asr_lock: - if not ( - self._asr_ready - and self._asr is not None - and self._asr.status == "ready" - ): - self._asr_ready = False - return - - compute = self._config["asr"]["compute_type"] - if engine_type == "whisper": - signature_model = cache_model_key - elif engine_type == "funasr": - signature_model = funasr_model - elif engine_type == "crispasr": - signature_model = ( - cache_model_key, - crispasr_backend, - crispasr_gpu_backend, - crispasr_device_index, - crispasr_punc_model, - crispasr_unified_memory, - ) - else: - signature_model = engine_type - language = settings.get( - "asr_language", self._config["asr"].get("language", "auto") - ) - signature = (engine_type, signature_model, device, hub, compute, language) - - with self._asr_lock: - current_asr = self._asr - current_ready = ( - self._asr_ready - and current_asr is not None - and current_asr.status == "ready" - ) - if current_ready and self._asr_signature == signature: - return - if not current_ready: - self._asr_ready = False + plan = self._asr_service.prepare_switch(engine_type, settings) + if plan.already_current: + return - log.info(f"Switching ASR worker: {self._asr_type} -> {engine_type}") # Reset interim state for the engine boundary. The active worker is # stopped before the target worker starts loading. self._interim_active = False @@ -562,175 +357,38 @@ def _switch_asr_engine(self, engine_type: str): self._vad.flush() self._vad.reset() - cached = is_asr_cached(engine_type, cache_model_key, hub) - display_name = ASR_DISPLAY_NAMES.get(engine_type, engine_type) - if engine_type == "whisper": - display_model = ( - local_faster_whisper_display_name(model_size) - if model_path - else model_size - ) or Path(model_size).name - display_name = f"Whisper {display_model}" - elif engine_type == "funasr": - display_name = funasr_display_name(funasr_model) - elif engine_type == "crispasr": - custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) - if custom_crispasr_path: - display_model = ( - local_crispasr_display_name(crispasr_model) - or Path(crispasr_model).name - ) - display_name = f"CrispASR {display_model}" - else: - display_name = f"CrispASR {crispasr_profile(crispasr_model)['display_name']}" - - profile = ( - {} - if custom_crispasr_path - else crispasr_profile(crispasr_model) - ) - if ( - profile.get("needs_punctuation") - and crispasr_punc_model in (None, "", "auto") - ): - crispasr_punc_model = "auto" - parent = ( self._panel if self._panel and self._panel.isVisible() else self._overlay ) - worker_config = { - "engine_type": engine_type, - "funasr_model": funasr_model, - "model_size": cache_model_key, - "device": device, - "compute_type": compute, - "hub": hub, - "language": language, - "pad_seconds": ( - settings.get( - "sensevoice_pad_seconds", - self._config["asr"].get("sensevoice_pad_seconds", 0.5), - ) - if engine_type == "funasr" - else settings.get( - "whisper_pad_seconds", - self._config["asr"].get("whisper_pad_seconds", 0.5), - ) - if engine_type == "whisper" - else None - ), - "download_root": str((MODELS_DIR / "huggingface" / "hub").resolve()), - "display_name": display_name, - } - if engine_type == "crispasr": - worker_config.update( - { - "crispasr_model": crispasr_model, - "crispasr_model_path": crispasr_model_path_value, - "crispasr_backend": crispasr_backend, - "crispasr_gpu_backend": crispasr_gpu_backend, - "crispasr_device_index": crispasr_device_index, - "crispasr_punc_model": crispasr_punc_model, - "crispasr_unified_memory": crispasr_unified_memory, - } + if plan.missing_models: + dlg = ModelDownloadDialog( + plan.missing_models, + hub=plan.hub, + proxy=plan.download_proxy, + parent=parent, ) - target_state = { - "type": engine_type, - "signature": signature, - "device": device, - "funasr_model_key": funasr_model - if engine_type == "funasr" - else self._funasr_model_key, - "whisper_model_size": model_size - if engine_type == "whisper" - else self._whisper_model_size, - "crispasr_model_key": crispasr_model - if engine_type == "crispasr" - else self._crispasr_model_key, - "config": worker_config, - "display_name": display_name, - } - - if not cached: - missing = get_missing_models(engine_type, cache_model_key, hub) - missing = [m for m in missing if m["type"] != "silero-vad"] - if missing: - dlg = ModelDownloadDialog( - missing, hub=hub, proxy=download_proxy, parent=parent + if dlg.exec() != QDialog.DialogCode.Accepted: + log.info(f"Download cancelled/failed: {plan.engine_type}") + self._asr_service.mark_download_cancelled() + return + error = self._asr_service.complete_download(plan) + if error: + QMessageBox.warning( + parent, + t("error_title"), + t("error_load_asr").format(error=error), ) - if dlg.exec() != QDialog.DialogCode.Accepted: - log.info(f"Download cancelled/failed: {engine_type}") - with self._asr_lock: - self._asr_ready = ( - self._asr is not None and self._asr.status == "ready" - ) - return - if engine_type == "crispasr": - crispasr_model_path_value = get_local_model_path( - "crispasr", hub=hub, funasr_model=cache_model_key - ) - if not crispasr_model_path_value: - QMessageBox.warning( - parent, - t("error_title"), - t("error_load_asr").format( - error="CrispASR model file was not found after download" - ), - ) - return - worker_config["crispasr_model_path"] = crispasr_model_path_value - target_state["config"] = worker_config - - with self._asr_lock: - old_asr = self._asr - old_config = dict(self._asr_config) if self._asr_config else None - old_state = { - "type": self._asr_type, - "signature": self._asr_signature, - "device": self._asr_device, - "funasr_model_key": self._funasr_model_key, - "whisper_model_size": self._whisper_model_size, - "crispasr_model_key": self._crispasr_model_key, - "config": old_config, - "display_name": (old_config or {}).get("display_name"), - } - self._asr = None - self._asr_ready = False - self._asr_type = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 + return dlg = _ModelLoadDialog( - t("loading_model").format(name=display_name), parent=parent + t("loading_model").format(name=plan.display_name), parent=parent ) - new_asr = [None] - restored_asr = [None] - load_error = [None] - restore_error = [None] + switch_result = [None] def _load(): - if old_asr is not None: - log.info(f"Stopping old ASR worker before switch: pid={old_asr.pid}") - old_asr.shutdown() - self._release_memory_caches() - try: - new_asr[0] = self._load_asr_client(worker_config) - except Exception as e: - load_error[0] = str(e) - log.error(f"Failed to load ASR worker: {e}", exc_info=True) - if old_config: - try: - log.info("Restoring previous ASR worker after switch failure") - restored_asr[0] = self._load_asr_client(old_config) - except Exception as restore_exc: - restore_error[0] = str(restore_exc) - log.error( - f"Failed to restore previous ASR worker: {restore_exc}", - exc_info=True, - ) + switch_result[0] = self._asr_service.switch_worker(plan) thread = threading.Thread(target=_load, daemon=True) thread.start() @@ -749,28 +407,19 @@ def _check(): dlg.exec() poll_timer.stop() - def _activate_asr(client, state): - with self._asr_lock: - self._asr = client - self._asr_type = state["type"] - self._asr_signature = state["signature"] - self._asr_device = state["device"] - self._asr_config = dict(state["config"]) if state["config"] else None - self._funasr_model_key = state["funasr_model_key"] - self._whisper_model_size = state["whisper_model_size"] - self._crispasr_model_key = state["crispasr_model_key"] - self._asr_ready = True - self._asr_error_count = 0 - - if new_asr[0] is not None: - _activate_asr(new_asr[0], target_state) + result = switch_result[0] + if result is None: + return + + if result.status == "ready": if self._overlay: - self._overlay.update_asr_device(f"{display_name} [{device}]") - log.info(f"ASR worker ready: {engine_type} on {device}") + self._overlay.update_asr_device( + f"{plan.display_name} [{plan.device}]" + ) return - if restored_asr[0] is not None: - _activate_asr(restored_asr[0], old_state) + if result.status == "restored": + old_state = result.restored_state or {} restored_name = old_state.get("display_name") or old_state.get("type") if self._overlay: self._overlay.update_asr_device( @@ -781,22 +430,18 @@ def _activate_asr(client, state): t("error_title"), t("error_load_asr").format( error=( - f"{load_error[0] or 'unknown error'}\n" + f"{result.load_error or 'unknown error'}\n" f"{t('asr_restore_succeeded')}" ) ), ) - log.info( - f"Previous ASR worker restored: " - f"{old_state.get('type')} on {old_state.get('device')}" - ) return - error = load_error[0] or "unknown error" - if restore_error[0]: + error = result.load_error or "unknown error" + if result.restore_error: error = ( f"{error}\n" - f"{t('asr_restore_failed').format(error=restore_error[0])}" + f"{t('asr_restore_failed').format(error=result.restore_error)}" ) QMessageBox.warning( parent, @@ -853,29 +498,16 @@ def _release_memory_caches(self): def _run_asr(self, audio: np.ndarray, kind: str, **kwargs): audio_seconds = len(audio) / 16000 asr_start = time.perf_counter() - with self._asr_lock: - if not self._asr_ready or self._asr is None: - return None, 0.0 - client = self._asr - try: - result = client.transcribe(audio, **kwargs) - except (ASRWorkerExited, ASRWorkerTimeout) as exc: - asr_ms = (time.perf_counter() - asr_start) * 1000 - self._log_mem_after_asr(f"{kind}:error", audio_seconds, asr_ms) - self._mark_asr_unavailable(str(exc), client) - raise - except ASRWorkerError as exc: - asr_ms = (time.perf_counter() - asr_start) * 1000 - self._log_mem_after_asr(f"{kind}:error", audio_seconds, asr_ms) - self._asr_error_count += 1 - if not exc.recoverable or self._asr_error_count >= 3: - self._mark_asr_unavailable(str(exc), client) - raise - except Exception: - asr_ms = (time.perf_counter() - asr_start) * 1000 - self._log_mem_after_asr(f"{kind}:error", audio_seconds, asr_ms) - raise - self._asr_error_count = 0 + if not self._asr_service.is_ready: + return None, 0.0 + try: + result = self._asr_service.transcribe(audio, **kwargs) + except Exception: + asr_ms = (time.perf_counter() - asr_start) * 1000 + self._log_mem_after_asr(f"{kind}:error", audio_seconds, asr_ms) + raise + if result is None: + return None, 0.0 asr_ms = (time.perf_counter() - asr_start) * 1000 self._log_mem_after_asr(kind, audio_seconds, asr_ms) return result, asr_ms @@ -1046,11 +678,11 @@ def stop(self): # Flush remaining VAD buffer after pipeline threads are done if self._interim_active: remaining = self._vad.force_flush() - if remaining is not None and self._asr_ready: + if remaining is not None and self._asr_service.is_ready: self._process_interim_final(remaining) else: remaining = self._vad.flush() - if remaining is not None and self._asr_ready: + if remaining is not None and self._asr_service.is_ready: self._process_segment(remaining) self._interim_active = False self._interim_pending = "" @@ -1072,7 +704,7 @@ def stop(self): f"GPU(alloc/reserved)={snap['gpu_alloc']:.0f}/{snap['gpu_reserved']:.0f}MB " f"asr_calls={self._mem_asr_call_count} outputs={self._asr_count}" ) - self._shutdown_asr_worker() + self._asr_service.shutdown() log.info("Pipeline stopped") def pause(self): @@ -1474,7 +1106,7 @@ def _capture_loop(self): for _ in range(n): with self._vad_lock: seg = self._vad.process_chunk(silence_chunk) - if seg is not None and self._asr_ready: + if seg is not None and self._asr_service.is_ready: self._enqueue_asr("vad_flush", seg) break continue @@ -1494,7 +1126,7 @@ def _capture_loop(self): if speech_segment is None: # Still accumulating — check for interim ASR - if (self._incremental_enabled and self._asr_ready + if (self._incremental_enabled and self._asr_service.is_ready and self._vad.is_speaking): buf_samples = self._vad.speech_samples total_dur = buf_samples / 16000 @@ -1506,7 +1138,7 @@ def _capture_loop(self): self._enqueue_asr("interim", None) continue - if not self._asr_ready: + if not self._asr_service.is_ready: log.debug("ASR not ready, dropping segment") continue @@ -2029,7 +1661,7 @@ def _on_overlay_lang_changed(lang_code): _asr_lang_actions[current_asr_lang].setChecked(True) def _on_tray_asr_lang(code): - live_trans._set_asr_language(code) + live_trans._asr_service.set_language(code) panel.set_asr_language(code) overlay.set_source_language(code) if code in _asr_lang_actions: @@ -2072,7 +1704,7 @@ def _on_overlay_source_lang(code): def _on_panel_asr_lang_changed(code): """Panel ASR language combo → sync to runtime, overlay, and tray.""" - live_trans._set_asr_language(code) + live_trans._asr_service.set_language(code) overlay.set_source_language(code) if code in _asr_lang_actions: _asr_lang_actions[code].setChecked(True) From 1925d3f47c9af46f7581b9b969f74f7175d15f55 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 05:49:17 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20=E6=8A=BD=E5=87=BA=20PipelineCont?= =?UTF-8?q?roller=20=E7=AE=A1=E7=90=86=E8=BF=90=E8=A1=8C=E6=97=B6=E7=AE=A1?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将音频采集、VAD、ASR 队列线程和增量 ASR 协调迁移到新的 pipeline_controller.py。 LiveTranslateApp 现在通过 PipelineController 事件接收音频电平和 ASR 文本,只保留 ASR 调用包装、内存日志、翻译调度和 UI 更新职责。 ASR 切换和设置同步改为调用 PipelineController 的公开方法,避免 main.py 直接管理 capture/asr 线程、队列和 VAD 状态。 同步 CLAUDE.md 的线程模型说明,并补齐 control_panel.py 中已有 Path 使用缺失的导入。 --- CLAUDE.md | 9 +- control_panel.py | 1 + main.py | 569 +++---------------------------------- pipeline_controller.py | 628 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 679 insertions(+), 528 deletions(-) create mode 100644 pipeline_controller.py diff --git a/CLAUDE.md b/CLAUDE.md index 1e69787..d14fc67 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,7 @@ The pipeline runs in a background thread: **Audio Capture (32ms chunks) -> VAD - ``` main.py (LiveTranslateApp) |-- model_manager.py Centralized model detection, download, cache utils + |-- pipeline_controller.py Audio capture + VAD + ASR queue controller, incremental ASR coordination |-- audio_capture.py WASAPI loopback via pyaudiowpatch, auto-reconnects on device change |-- vad_processor.py Silero VAD / energy-based / disabled modes, progressive silence + backtrack split |-- asr_client.py Main-process ASR worker manager (spawn, Pipe IPC, timeouts) @@ -46,12 +47,12 @@ main.py (LiveTranslateApp) ### Threading / Process Model - **Main thread**: Qt event loop (all UI) -- **Capture thread**: `_capture_loop` in `LiveTranslateApp` reads audio and runs VAD -- **ASR queue thread**: `_asr_loop` drains VAD segments and calls `ASRClient.transcribe()` +- **Capture thread**: `_capture_loop` in `PipelineController` reads audio and runs VAD +- **ASR queue thread**: `_asr_loop` in `PipelineController` drains VAD segments and calls ASR via `ASRService` - **ASR worker process**: `asr_worker.py` owns the concrete ASR backend/model and runs inference over `multiprocessing.Pipe` - **ASR loading**: `_switch_asr_engine()` stops the current worker, then starts the target worker in a background thread; if target loading fails, it restarts the previous worker from its saved config - Cross-thread UI updates use **Qt signals** (e.g., `add_message_signal`, `update_translation_signal`) -- ASR readiness tracked by `_asr_ready` flag; pipeline drops segments while no ready worker exists +- ASR readiness tracked by `ASRService`; `PipelineController` drops segments while no ready worker exists ### Configuration @@ -175,7 +176,7 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr - Whisper (ctranslate2) only accepts `device="cuda"` not `"cuda:0"`; device index passed via `device_index` param. Parsed from combo text like `"cuda:0 (RTX 4090)"` in `_switch_asr_engine` - ASR text density filter: segments ≥2s producing ≤3 alnum characters are discarded as noise - Settings file uses atomic write (write to `.tmp` then `os.replace`) to prevent corruption on crash -- `stop()` joins pipeline thread before flushing VAD to prevent concurrent `_process_segment` calls +- `PipelineController.stop()` joins capture/ASR queue threads before flushing VAD to prevent concurrent segment processing - Cancelled ASR download leaves the current worker running; failed target worker load attempts to restore the previous worker config - `Translator._build_system_prompt` catches format errors in user prompt templates, falls back to DEFAULT_PROMPT - Translation prompt presets: `PROMPT_PRESETS` in `translator.py` (daily/esports/anime), selectable via control panel combo diff --git a/control_panel.py b/control_panel.py index 9043f0b..bf80608 100644 --- a/control_panel.py +++ b/control_panel.py @@ -1,6 +1,7 @@ import logging import os import threading +from pathlib import Path from PyQt6.QtCore import Qt, QTimer, pyqtSignal from PyQt6.QtGui import QFont diff --git a/main.py b/main.py index 2cd8300..b18a492 100644 --- a/main.py +++ b/main.py @@ -7,12 +7,10 @@ import signal import logging import threading -import queue import gc from concurrent.futures import ThreadPoolExecutor import yaml import time -import numpy as np from pathlib import Path from datetime import datetime @@ -32,9 +30,8 @@ # torch must be imported before PyQt6 to avoid DLL conflicts on Windows import torch # noqa: F401 -from audio_capture import AudioCapture -from vad_processor import VADProcessor from asr_service import ASRService +from pipeline_controller import PipelineController from translator import Translator, RepetitionError from transcript_writer import TranscriptWriter @@ -147,24 +144,19 @@ def __init__(self, config): self._running = False self._paused = False - self._audio = AudioCapture( - device=config["audio"].get("device"), - sample_rate=config["audio"]["sample_rate"], - chunk_duration=config["audio"]["chunk_duration"], - ) - self._vad = VADProcessor( - sample_rate=config["audio"]["sample_rate"], - threshold=config["asr"]["vad_threshold"], - min_speech_duration=config["asr"]["min_speech_duration"], - max_speech_duration=config["asr"]["max_speech_duration"], - chunk_duration=config["audio"]["chunk_duration"], - ) self._asr_service = ASRService( config, release_memory_caches=self._release_memory_caches, unavailable_callback=self._on_asr_unavailable, ) - self._vad_lock = threading.Lock() + self._pipeline = PipelineController( + config, + asr_runner=self._run_asr, + asr_ready=lambda: self._asr_service.is_ready, + asr_language=self._current_asr_language, + audio_level_callback=self._on_audio_level, + asr_text_callback=self._on_asr_text, + ) self._target_language = config["translation"]["target_language"] self._translator = Translator( api_base=config["translation"]["api_base"], @@ -182,9 +174,6 @@ def __init__(self, config): self._overlay = None self._subwin = None self._panel = None - self._capture_thread = None - self._asr_thread = None - self._asr_queue = queue.Queue(maxsize=16) self._tl_executor = ThreadPoolExecutor(max_workers=8) self._transcript = TranscriptWriter(Path(__file__).parent / "transcripts") @@ -212,15 +201,6 @@ def __init__(self, config): self._last_original = "" self._last_msg_id = 0 - # Incremental ASR state - self._incremental_enabled = False - self._interim_interval = 2.0 - self._interim_pending = "" - self._interim_active = False - self._last_interim_samples = 0 - self._last_interim_check_time = 0.0 - self._interim_committed_tail = "" - def set_overlay(self, overlay: SubtitleOverlay): self._overlay = overlay @@ -237,8 +217,22 @@ def _on_models_list_changed(self, models: list, active_idx: int): if self._overlay: self._overlay.set_models(models, active_idx) + def _current_asr_language(self) -> str: + if self._panel: + return self._panel.get_settings().get("asr_language", "auto") + return "auto" + + def _on_audio_level(self, event): + if self._overlay: + self._overlay.update_monitor( + event.rms, event.vad_confidence, event.mic_rms + ) + + def _on_asr_text(self, event): + self._handle_asr_text(event.text, event.source_lang, event.asr_ms) + def _on_settings_changed(self, settings): - self._vad.update_settings(settings) + self._pipeline.apply_settings(settings) if "style" in settings and self._overlay: self._overlay.apply_style(settings["style"]) if "asr_language" in settings: @@ -270,20 +264,6 @@ def _on_settings_changed(self, settings): or self._config["asr"].get("asr_engine", "funasr"), ) ) - if "audio_device" in settings: - old_device = self._audio.device_name - self._audio.set_device(settings["audio_device"]) - if old_device != settings.get("audio_device"): - self._vad.flush() - self._vad.reset() - if self._overlay: - self._overlay.update_monitor(0.0, 0.0) - if "mic_device" in settings: - self._audio.set_mic_device(settings["mic_device"]) - if "incremental_asr" in settings: - self._incremental_enabled = settings["incremental_asr"] - if "interim_interval" in settings: - self._interim_interval = settings["interim_interval"] if "target_language" in settings: self._target_language = settings["target_language"] if self._overlay: @@ -349,13 +329,7 @@ def _switch_asr_engine(self, engine_type: str): # Reset interim state for the engine boundary. The active worker is # stopped before the target worker starts loading. - self._interim_active = False - self._interim_pending = "" - self._last_interim_samples = 0 - self._last_interim_check_time = 0.0 - self._interim_committed_tail = "" - self._vad.flush() - self._vad.reset() + self._pipeline.reset_for_asr_switch() parent = ( self._panel if self._panel and self._panel.isVisible() else self._overlay @@ -463,7 +437,7 @@ def _mem_snapshot(self) -> dict: except Exception: pass msgs = len(self._overlay._messages) if self._overlay else 0 - vad_buf = self._vad.buffer_stats()["chunks"] + vad_buf = self._pipeline.buffer_stats()["chunks"] return { "rss": rss_mb, "gpu_alloc": gpu_alloc_mb, @@ -495,8 +469,8 @@ def _release_memory_caches(self): except Exception: pass - def _run_asr(self, audio: np.ndarray, kind: str, **kwargs): - audio_seconds = len(audio) / 16000 + def _run_asr(self, audio, kind: str, **kwargs): + audio_seconds = len(audio) / self._config["audio"]["sample_rate"] asr_start = time.perf_counter() if not self._asr_service.is_ready: return None, 0.0 @@ -638,18 +612,9 @@ def start(self): return n = len(self._subwin.get_target_languages()) if self._subwin else 1 self._tl_executor = ThreadPoolExecutor(max_workers=max(8, n + 1)) - self._asr_queue = queue.Queue(maxsize=16) + self._pipeline.start() self._running = True self._paused = False - self._audio.start() - self._capture_thread = threading.Thread( - target=self._capture_loop, daemon=True - ) - self._asr_thread = threading.Thread( - target=self._asr_loop, daemon=True - ) - self._capture_thread.start() - self._asr_thread.start() # Periodic memory snapshot every 30s if self._mem_periodic_timer is None: self._mem_periodic_timer = QTimer() @@ -661,34 +626,10 @@ def start(self): f"GPU(alloc/reserved)={snap['gpu_alloc']:.0f}/{snap['gpu_reserved']:.0f}MB " f"(baseline for delta tracking)" ) - log.info("Pipeline started (capture + ASR threads)") def stop(self): self._running = False - self._audio.stop() - if self._capture_thread: - self._capture_thread.join(timeout=3) - self._capture_thread = None - self._asr_queue.put(None) - if self._asr_thread: - self._asr_thread.join(timeout=10) - if self._asr_thread.is_alive(): - log.warning("ASR thread still running after timeout, proceeding with cleanup") - self._asr_thread = None - # Flush remaining VAD buffer after pipeline threads are done - if self._interim_active: - remaining = self._vad.force_flush() - if remaining is not None and self._asr_service.is_ready: - self._process_interim_final(remaining) - else: - remaining = self._vad.flush() - if remaining is not None and self._asr_service.is_ready: - self._process_segment(remaining) - self._interim_active = False - self._interim_pending = "" - self._last_interim_samples = 0 - self._last_interim_check_time = 0.0 - self._interim_committed_tail = "" + self._pipeline.stop() self._tl_executor.shutdown(wait=True) self._transcript.close() if self._mem_periodic_timer is not None: @@ -705,70 +646,20 @@ def stop(self): f"asr_calls={self._mem_asr_call_count} outputs={self._asr_count}" ) self._asr_service.shutdown() - log.info("Pipeline stopped") def pause(self): self._paused = True - self._interim_active = False - self._interim_pending = "" - self._last_interim_samples = 0 - self._last_interim_check_time = 0.0 - self._interim_committed_tail = "" - if self._overlay: - self._overlay.update_monitor(0.0, 0.0) - log.info("Pipeline paused") + self._pipeline.pause() def resume(self): self._paused = False - log.info("Pipeline resumed") - - def _process_segment(self, speech_segment): - """Run ASR + translation on a speech segment. Called from ASR thread and stop().""" - seg_len = len(speech_segment) / 16000 - log.info(f"Speech segment: {seg_len:.1f}s") - - try: - result, asr_ms = self._run_asr(speech_segment, "segment") - except Exception as e: - log.error(f"ASR error: {e}", exc_info=True) - return - if asr_ms == 0: - return - if asr_ms > 10000: - log.warning(f"ASR took {asr_ms:.0f}ms, possible hang") - if result is None: - return - - original_text = result["text"].strip() - # Skip empty or punctuation-only ASR results - if not original_text or not any(c.isalnum() for c in original_text): - log.debug( - f"ASR returned empty/punctuation-only, skipping: '{result['text']}'" - ) - return - - # Skip suspiciously short text from long segments (likely noise) - alnum_chars = sum(1 for c in original_text if c.isalnum()) - if seg_len >= 2.0 and alnum_chars <= 3: - log.debug( - f"Noise filter: {seg_len:.1f}s segment produced only '{original_text}', skipping" - ) - return - - source_lang = result["language"] - asr_lang_setting = self._panel.get_settings().get("asr_language", "auto") if self._panel else "auto" - if asr_lang_setting != "auto" and source_lang != asr_lang_setting: - log.info( - f"Language filter: expected '{asr_lang_setting}' but got '{source_lang}', " - f"discarding: {original_text[:60]}" - ) - return + self._pipeline.resume() + def _handle_asr_text(self, original_text: str, source_lang: str, asr_ms: float = 0): self._asr_count += 1 self._msg_id += 1 msg_id = self._msg_id timestamp = datetime.now().strftime("%H:%M:%S") - log.info(f"ASR [{source_lang}] ({asr_ms:.0f}ms): {original_text}") if self._overlay: self._overlay.add_message( @@ -776,17 +667,15 @@ def _process_segment(self, speech_segment): ) self._transcript.write_original(msg_id, timestamp, original_text) - # Store for subtitle window (translation will be added later) + # Store for subtitle window; translation will be added later. self._last_original = original_text self._last_msg_id = msg_id target_lang = self._target_language - # Collect extra languages needed by subtitle window (beyond the primary target) extra_langs = set() if self._subwin and self._subwin.isVisible(): subwin_langs = self._subwin.get_target_languages() - # Remove primary target and source (no need to translate those) extra_langs = subwin_langs - {target_lang, source_lang} if source_lang == target_lang: @@ -802,400 +691,32 @@ def _process_segment(self, speech_segment): self._compute_cost(), ) if self._subwin and self._subwin.isVisible(): - # Primary is same language; still need to translate extra langs if extra_langs: try: self._tl_executor.submit( - self._translate_subwin_only, original_text, source_lang, extra_langs + self._translate_subwin_only, + original_text, + source_lang, + extra_langs, ) except RuntimeError: pass else: - self._subwin.update_text(original_text, {target_lang: original_text}) + self._subwin.update_text( + original_text, {target_lang: original_text} + ) else: try: self._tl_executor.submit( - self._translate_async, msg_id, original_text, source_lang, + self._translate_async, + msg_id, + original_text, + source_lang, extra_langs or None, ) except RuntimeError: log.warning("Translation executor shut down, skipping") - # ── Incremental ASR ── - - _pysbd_cache = {} # lang -> pysbd.Segmenter - - @staticmethod - def _get_segmenter(lang: str): - import pysbd - if lang not in LiveTranslateApp._pysbd_cache: - pysbd_lang = lang if lang in pysbd.languages.LANGUAGE_CODES else "en" - LiveTranslateApp._pysbd_cache[lang] = pysbd.Segmenter( - language=pysbd_lang, clean=False - ) - return LiveTranslateApp._pysbd_cache[lang] - - def _split_sentences(self, text: str, lang: str = "en") -> list[str]: - """Split text into sentences using pysbd, with comma fallback for long text.""" - seg = self._get_segmenter(lang) - parts = [p for p in seg.segment(text) if p.strip()] - if len(parts) > 1: - return parts - - # Comma fallback for long unsplit text — split at last balanced comma - # CJK 「、」at 25 chars; all commas at 60 chars (long sentence, reduce latency) - min_len = 25 if any(c == '、' for c in text) else 60 - if len(text) > min_len: - for i in range(len(text) - 8, 5, -1): - if text[i] in ',,;;、': - before = text[:i + 1].strip() - after = text[i + 1:].strip() - if before and after and len(before) > 15 and len(after) > 3: - return [before, after] - - return parts - - @staticmethod - def _is_short_utterance(text: str) -> bool: - """Check if text has ≤8 alphanumeric chars (likely noise/filler/fragment).""" - alnum = sum(1 for c in text if c.isalnum()) - return alnum <= 8 - - def _strip_committed_overlap(self, text: str) -> str: - """Remove text that overlaps with previously committed content.""" - if not self._interim_committed_tail: - return text - tail = self._interim_committed_tail.lower().rstrip() - text_lower = text.lower() - # Check if text starts with a suffix of the committed tail - max_check = min(len(tail), len(text_lower)) - for overlap_len in range(max_check, 2, -1): - if text_lower[:overlap_len] == tail[-overlap_len:]: - stripped = text[overlap_len:].strip() - if stripped: - log.debug(f"Stripped echo overlap ({overlap_len} chars): '{text[:overlap_len]}...'") - return stripped - return "" - return text - - def _do_interim_asr(self) -> bool: - """Run ASR on current VAD buffer, output complete sentences, trim consumed audio. - Returns True if any sentences were committed.""" - with self._vad_lock: - peek = self._vad.peek_buffer() - if peek is None: - return False - audio, duration = peek - - # Don't bother with very short buffers - if duration < 1.5: - return False - - # Word timestamp alignment is expensive for repeated interim passes. - # The proportional trim path below is less exact but keeps long runs stable. - use_word_ts = False - - try: - result, asr_ms = self._run_asr( - audio, "interim", word_timestamps=use_word_ts - ) if use_word_ts else self._run_asr(audio, "interim") - except Exception as e: - log.error(f"Interim ASR error: {e}", exc_info=True) - return False - - if asr_ms == 0: - return False - - if result is None: - return False - - full_text = result["text"].strip() - if not full_text or not any(c.isalnum() for c in full_text): - return False - - # Strip echo from previous commit's overlap - full_text = self._strip_committed_overlap(full_text) - if not full_text: - return False - - split_start = time.perf_counter() - sentences = self._split_sentences(full_text, result["language"]) - split_ms = (time.perf_counter() - split_start) * 1000 - if len(sentences) <= 1: - return False - log.debug(f"Interim split [{result['language']}] ({split_ms:.1f}ms): {len(sentences)} parts -> {sentences}") - - # All but last are complete; last is still being spoken - complete = sentences[:-1] - - committed_text = "" - for sent in complete: - committed_text += sent - - if not committed_text.strip(): - return False - - # Determine trim point - total_samples = len(audio) - if use_word_ts and result.get("words"): - words = result["words"] - committed_lower = committed_text.lower().rstrip() - char_pos = 0 - last_word_end = 0.0 - for w in words: - word_text = w["word"].strip() - idx = committed_lower.find(word_text.lower(), char_pos) - if idx >= 0: - char_pos = idx + len(word_text) - last_word_end = w["end"] - if char_pos >= len(committed_lower): - break - trim_samples = int(last_word_end * 16000) - else: - # Proportional trim with safety margin to reduce echo - ratio = len(committed_text) / max(len(full_text), 1) - margin = int(0.3 * 16000) # 0.3s extra trim to avoid re-recognition - trim_samples = int(ratio * total_samples) + margin - # Don't over-trim: keep at least 0.5s for the remaining sentence - max_trim = total_samples - int(0.5 * 16000) - trim_samples = min(trim_samples, max(max_trim, 0)) - # Minimum trim to prevent re-recognition loops - min_trim = int(0.3 * 16000) - if trim_samples < min_trim and trim_samples > 0: - trim_samples = min(min_trim, total_samples // 2) - - # Output committed sentences - actually_committed = False - for sent in complete: - text = sent.strip() - if not text: - continue - if self._is_short_utterance(text): - self._interim_pending += text - log.debug(f"Interim short utterance buffered: '{text}', pending='{self._interim_pending}'") - continue - - if self._interim_pending: - text = self._interim_pending + text - self._interim_pending = "" - - self._process_segment_text(text, result["language"], asr_ms) - actually_committed = True - - if not actually_committed: - return False - - if trim_samples > 0: - with self._vad_lock: - self._vad.trim_front(trim_samples) - - # Track committed text tail for echo dedup - self._interim_committed_tail = committed_text[-50:] if len(committed_text) > 50 else committed_text - - self._interim_active = True - log.info(f"Interim ASR: committed {len(complete)} sentence(s), trimmed {trim_samples / 16000:.2f}s") - return True - - def _process_segment_text(self, text: str, source_lang: str, asr_ms: float = 0): - """Output a text result (from interim or final) — similar to _process_segment but skips ASR.""" - original_text = text.strip() - if not original_text or not any(c.isalnum() for c in original_text): - return - - asr_lang_setting = self._panel.get_settings().get("asr_language", "auto") if self._panel else "auto" - if asr_lang_setting != "auto" and source_lang != asr_lang_setting: - log.info(f"Language filter: expected '{asr_lang_setting}' but got '{source_lang}', discarding: {original_text[:60]}") - return - - self._asr_count += 1 - self._msg_id += 1 - msg_id = self._msg_id - timestamp = datetime.now().strftime("%H:%M:%S") - log.info(f"ASR [{source_lang}] ({asr_ms:.0f}ms, interim): {original_text}") - - if self._overlay: - self._overlay.add_message(msg_id, timestamp, original_text, source_lang, asr_ms) - self._transcript.write_original(msg_id, timestamp, original_text) - - self._last_original = original_text - self._last_msg_id = msg_id - - target_lang = self._target_language - extra_langs = set() - if self._subwin and self._subwin.isVisible(): - subwin_langs = self._subwin.get_target_languages() - extra_langs = subwin_langs - {target_lang, source_lang} - - if source_lang == target_lang: - log.info(f"Same language ({source_lang}), no translation") - self._transcript.finalize_no_translation(msg_id) - if self._overlay: - self._overlay.update_translation(msg_id, "", 0) - self._overlay.update_stats(self._asr_count, self._translate_count, self._total_prompt_tokens, self._total_completion_tokens, self._compute_cost()) - if self._subwin and self._subwin.isVisible(): - if extra_langs: - try: - self._tl_executor.submit(self._translate_subwin_only, original_text, source_lang, extra_langs) - except RuntimeError: - pass - else: - self._subwin.update_text(original_text, {target_lang: original_text}) - else: - try: - self._tl_executor.submit(self._translate_async, msg_id, original_text, source_lang, extra_langs or None) - except RuntimeError: - log.warning("Translation executor shut down, skipping") - def _process_interim_final(self, speech_segment): - """Handle VAD flush after interim outputs were already made.""" - seg_len = len(speech_segment) / 16000 - log.info(f"Interim final segment: {seg_len:.1f}s") - - try: - result, asr_ms = self._run_asr(speech_segment, "interim_final") - except Exception as e: - log.error(f"Interim final ASR error: {e}", exc_info=True) - return - if asr_ms == 0: - return - - if result is None: - # Flush any remaining pending - if self._interim_pending: - text = self._interim_pending - self._interim_pending = "" - lang = self._panel.get_settings().get("asr_language", "auto") if self._panel else "auto" - if lang == "auto": - lang = "unknown" - self._process_segment_text(text, lang) - return - - original_text = result["text"].strip() - - # Strip echo from previous commit's overlap - original_text = self._strip_committed_overlap(original_text) - - # Prepend any remaining pending short utterances - if self._interim_pending: - original_text = self._interim_pending + original_text - self._interim_pending = "" - - if not original_text or not any(c.isalnum() for c in original_text): - return - - # Apply noise filter like _process_segment - alnum_chars = sum(1 for c in original_text if c.isalnum()) - if seg_len >= 2.0 and alnum_chars <= 3: - log.debug(f"Noise filter: {seg_len:.1f}s segment produced only '{original_text}', skipping") - return - - self._process_segment_text(original_text, result["language"], asr_ms) - - def _capture_loop(self): - silence_chunk = np.zeros( - int( - self._config["audio"]["sample_rate"] - * self._config["audio"]["chunk_duration"] - ), - dtype=np.float32, - ) - while self._running: - item = self._audio.get_audio(timeout=1.0) - if item is None: - if self._vad.is_speaking and not self._paused: - n = self._vad.effective_silence_limit_chunks() + 1 - for _ in range(n): - with self._vad_lock: - seg = self._vad.process_chunk(silence_chunk) - if seg is not None and self._asr_service.is_ready: - self._enqueue_asr("vad_flush", seg) - break - continue - - chunk, mic_rms = item - - if self._paused: - continue - - rms = float(np.sqrt(np.mean(chunk**2))) - - if self._overlay: - self._overlay.update_monitor(rms, self._vad.last_confidence, mic_rms) - - with self._vad_lock: - speech_segment = self._vad.process_chunk(chunk) - - if speech_segment is None: - # Still accumulating — check for interim ASR - if (self._incremental_enabled and self._asr_service.is_ready - and self._vad.is_speaking): - buf_samples = self._vad.speech_samples - total_dur = buf_samples / 16000 - elapsed = (buf_samples - self._last_interim_samples) / 16000 - now = time.perf_counter() - cooldown = now - self._last_interim_check_time - if total_dur >= self._interim_interval and elapsed >= self._interim_interval and cooldown >= 1.0: - self._last_interim_check_time = now - self._enqueue_asr("interim", None) - continue - - if not self._asr_service.is_ready: - log.debug("ASR not ready, dropping segment") - continue - - self._enqueue_asr("vad_flush", speech_segment) - - def _enqueue_asr(self, seg_type: str, segment): - try: - self._asr_queue.put_nowait((seg_type, segment)) - except queue.Full: - try: - dropped = self._asr_queue.get_nowait() - log.warning(f"ASR queue full, dropped {dropped[0]} segment") - except queue.Empty: - pass - try: - self._asr_queue.put_nowait((seg_type, segment)) - except queue.Full: - log.warning("ASR queue still full after drop, skipping segment") - - def _asr_loop(self): - while self._running: - try: - item = self._asr_queue.get(timeout=1.0) - except queue.Empty: - continue - - if item is None: - break - - seg_type, segment = item - - if seg_type == "vad_flush": - if self._interim_active: - self._process_interim_final(segment) - else: - self._process_segment(segment) - self._interim_active = False - self._interim_pending = "" - self._last_interim_samples = 0 - self._last_interim_check_time = 0.0 - self._interim_committed_tail = "" - elif seg_type == "interim": - self._drain_interim_duplicates() - self._do_interim_asr() - with self._vad_lock: - self._last_interim_samples = self._vad.speech_samples - - def _drain_interim_duplicates(self): - while True: - try: - item = self._asr_queue.get_nowait() - except queue.Empty: - break - if item is None or item[0] != "interim": - self._asr_queue.put(item) - break - def main(): setup_logging() diff --git a/pipeline_controller.py b/pipeline_controller.py new file mode 100644 index 0000000..f6ea7f5 --- /dev/null +++ b/pipeline_controller.py @@ -0,0 +1,628 @@ +import logging +import queue +import threading +import time +from dataclasses import dataclass +from typing import Callable + +import numpy as np + +from audio_capture import AudioCapture +from vad_processor import VADProcessor + + +log = logging.getLogger("LiveTranslate.Pipeline") + + +@dataclass +class AudioLevelEvent: + rms: float + vad_confidence: float + mic_rms: float | None = None + + +@dataclass +class SpeechSegmentEvent: + kind: str + audio_seconds: float + + +@dataclass +class ASRTextEvent: + text: str + source_lang: str + asr_ms: float + interim: bool = False + + +@dataclass +class ASRErrorEvent: + kind: str + error: Exception + + +@dataclass +class PipelineStatsEvent: + state: str + queue_size: int = 0 + + +class PipelineController: + """Owns audio capture, VAD, ASR queueing, and incremental ASR coordination.""" + + _pysbd_cache = {} + + def __init__( + self, + config: dict, + asr_runner: Callable[..., tuple[dict | None, float]], + asr_ready: Callable[[], bool], + asr_language: Callable[[], str], + audio_level_callback: Callable[[AudioLevelEvent], None] | None = None, + speech_segment_callback: Callable[[SpeechSegmentEvent], None] | None = None, + asr_text_callback: Callable[[ASRTextEvent], None] | None = None, + asr_error_callback: Callable[[ASRErrorEvent], None] | None = None, + stats_callback: Callable[[PipelineStatsEvent], None] | None = None, + ): + self._config = config + self._sample_rate = config["audio"]["sample_rate"] + self._chunk_duration = config["audio"]["chunk_duration"] + + self._asr_runner = asr_runner + self._asr_ready = asr_ready + self._asr_language = asr_language + self._audio_level_callback = audio_level_callback + self._speech_segment_callback = speech_segment_callback + self._asr_text_callback = asr_text_callback + self._asr_error_callback = asr_error_callback + self._stats_callback = stats_callback + + self._audio = AudioCapture( + device=config["audio"].get("device"), + sample_rate=self._sample_rate, + chunk_duration=self._chunk_duration, + ) + self._vad = VADProcessor( + sample_rate=self._sample_rate, + threshold=config["asr"]["vad_threshold"], + min_speech_duration=config["asr"]["min_speech_duration"], + max_speech_duration=config["asr"]["max_speech_duration"], + chunk_duration=self._chunk_duration, + ) + + self._vad_lock = threading.Lock() + self._asr_queue = queue.Queue(maxsize=16) + self._capture_thread = None + self._asr_thread = None + self._running = False + self._paused = False + + self._incremental_enabled = False + self._interim_interval = 2.0 + self._interim_pending = "" + self._interim_active = False + self._last_interim_samples = 0 + self._last_interim_check_time = 0.0 + self._interim_committed_tail = "" + + @property + def is_running(self) -> bool: + return self._running + + @property + def is_paused(self) -> bool: + return self._paused + + def start(self): + if self._running: + return + self._asr_queue = queue.Queue(maxsize=16) + self._paused = False + self._audio.start() + self._running = True + self._capture_thread = threading.Thread( + target=self._capture_loop, daemon=True + ) + self._asr_thread = threading.Thread(target=self._asr_loop, daemon=True) + self._capture_thread.start() + self._asr_thread.start() + self._emit_stats("started") + log.info("Pipeline started (capture + ASR threads)") + + def stop(self): + self._running = False + self._audio.stop() + if self._capture_thread: + self._capture_thread.join(timeout=3) + self._capture_thread = None + + self._asr_queue.put(None) + if self._asr_thread: + self._asr_thread.join(timeout=10) + if self._asr_thread.is_alive(): + log.warning("ASR thread still running after timeout, proceeding") + self._asr_thread = None + + # Flush after the worker threads have stopped to avoid concurrent VAD access. + if self._interim_active: + with self._vad_lock: + remaining = self._vad.force_flush() + if remaining is not None and self._is_asr_ready(): + self._process_interim_final(remaining) + else: + with self._vad_lock: + remaining = self._vad.flush() + if remaining is not None and self._is_asr_ready(): + self._process_segment(remaining) + + self._reset_interim_state() + self._emit_stats("stopped") + log.info("Pipeline stopped") + + def pause(self): + self._paused = True + self._reset_interim_state() + self._emit_audio_level(0.0, 0.0) + self._emit_stats("paused") + log.info("Pipeline paused") + + def resume(self): + self._paused = False + self._emit_stats("resumed") + log.info("Pipeline resumed") + + def apply_settings(self, settings: dict): + self._vad.update_settings(settings) + + if "audio_device" in settings: + old_device = self._audio.device_name + self._audio.set_device(settings["audio_device"]) + if old_device != settings.get("audio_device"): + with self._vad_lock: + self._vad.flush() + self._vad.reset() + self._reset_interim_state() + self._emit_audio_level(0.0, 0.0) + + if "mic_device" in settings: + self._audio.set_mic_device(settings["mic_device"]) + + if "incremental_asr" in settings: + self._incremental_enabled = settings["incremental_asr"] + if "interim_interval" in settings: + self._interim_interval = settings["interim_interval"] + + def reset_for_asr_switch(self): + self._reset_interim_state() + with self._vad_lock: + self._vad.flush() + self._vad.reset() + + def buffer_stats(self) -> dict: + return self._vad.buffer_stats() + + def _capture_loop(self): + silence_chunk = np.zeros( + int(self._sample_rate * self._chunk_duration), + dtype=np.float32, + ) + while self._running: + item = self._audio.get_audio(timeout=1.0) + if item is None: + if self._vad.is_speaking and not self._paused: + n = self._vad.effective_silence_limit_chunks() + 1 + for _ in range(n): + with self._vad_lock: + seg = self._vad.process_chunk(silence_chunk) + if seg is not None and self._is_asr_ready(): + self._enqueue_asr("vad_flush", seg) + break + continue + + chunk, mic_rms = item + + if self._paused: + continue + + rms = float(np.sqrt(np.mean(chunk**2))) + self._emit_audio_level(rms, self._vad.last_confidence, mic_rms) + + with self._vad_lock: + speech_segment = self._vad.process_chunk(chunk) + + if speech_segment is None: + if ( + self._incremental_enabled + and self._is_asr_ready() + and self._vad.is_speaking + ): + buf_samples = self._vad.speech_samples + total_dur = buf_samples / self._sample_rate + elapsed = ( + buf_samples - self._last_interim_samples + ) / self._sample_rate + now = time.perf_counter() + cooldown = now - self._last_interim_check_time + if ( + total_dur >= self._interim_interval + and elapsed >= self._interim_interval + and cooldown >= 1.0 + ): + self._last_interim_check_time = now + self._enqueue_asr("interim", None) + continue + + if not self._is_asr_ready(): + log.debug("ASR not ready, dropping segment") + continue + + self._enqueue_asr("vad_flush", speech_segment) + + def _enqueue_asr(self, seg_type: str, segment): + try: + self._asr_queue.put_nowait((seg_type, segment)) + except queue.Full: + try: + dropped = self._asr_queue.get_nowait() + log.warning(f"ASR queue full, dropped {dropped[0]} segment") + except queue.Empty: + pass + try: + self._asr_queue.put_nowait((seg_type, segment)) + except queue.Full: + log.warning("ASR queue still full after drop, skipping segment") + + def _asr_loop(self): + while self._running: + try: + item = self._asr_queue.get(timeout=1.0) + except queue.Empty: + continue + + if item is None: + break + + seg_type, segment = item + + if seg_type == "vad_flush": + if self._interim_active: + self._process_interim_final(segment) + else: + self._process_segment(segment) + self._reset_interim_state() + elif seg_type == "interim": + self._drain_interim_duplicates() + self._do_interim_asr() + with self._vad_lock: + self._last_interim_samples = self._vad.speech_samples + + def _process_segment(self, speech_segment): + seg_len = len(speech_segment) / self._sample_rate + log.info(f"Speech segment: {seg_len:.1f}s") + self._emit_speech_segment("segment", seg_len) + + try: + result, asr_ms = self._run_asr(speech_segment, "segment") + except Exception as exc: + log.error(f"ASR error: {exc}", exc_info=True) + self._emit_asr_error("segment", exc) + return + if asr_ms == 0: + return + if asr_ms > 10000: + log.warning(f"ASR took {asr_ms:.0f}ms, possible hang") + if result is None: + return + + original_text = result["text"].strip() + if not original_text or not any(c.isalnum() for c in original_text): + log.debug( + f"ASR returned empty/punctuation-only, skipping: '{result['text']}'" + ) + return + + alnum_chars = sum(1 for c in original_text if c.isalnum()) + if seg_len >= 2.0 and alnum_chars <= 3: + log.debug( + f"Noise filter: {seg_len:.1f}s segment produced only " + f"'{original_text}', skipping" + ) + return + + source_lang = result["language"] + if not self._language_allowed(source_lang, original_text): + return + + log.info(f"ASR [{source_lang}] ({asr_ms:.0f}ms): {original_text}") + self._emit_asr_text(original_text, source_lang, asr_ms, interim=False) + + @staticmethod + def _get_segmenter(lang: str): + import pysbd + + if lang not in PipelineController._pysbd_cache: + pysbd_lang = lang if lang in pysbd.languages.LANGUAGE_CODES else "en" + PipelineController._pysbd_cache[lang] = pysbd.Segmenter( + language=pysbd_lang, clean=False + ) + return PipelineController._pysbd_cache[lang] + + def _split_sentences(self, text: str, lang: str = "en") -> list[str]: + seg = self._get_segmenter(lang) + parts = [p for p in seg.segment(text) if p.strip()] + if len(parts) > 1: + return parts + + min_len = 25 if any(c == "、" for c in text) else 60 + if len(text) > min_len: + for i in range(len(text) - 8, 5, -1): + if text[i] in ",,;;、": + before = text[: i + 1].strip() + after = text[i + 1 :].strip() + if before and after and len(before) > 15 and len(after) > 3: + return [before, after] + + return parts + + @staticmethod + def _is_short_utterance(text: str) -> bool: + alnum = sum(1 for c in text if c.isalnum()) + return alnum <= 8 + + def _strip_committed_overlap(self, text: str) -> str: + if not self._interim_committed_tail: + return text + tail = self._interim_committed_tail.lower().rstrip() + text_lower = text.lower() + max_check = min(len(tail), len(text_lower)) + for overlap_len in range(max_check, 2, -1): + if text_lower[:overlap_len] == tail[-overlap_len:]: + stripped = text[overlap_len:].strip() + if stripped: + log.debug( + f"Stripped echo overlap ({overlap_len} chars): " + f"'{text[:overlap_len]}...'" + ) + return stripped + return "" + return text + + def _do_interim_asr(self) -> bool: + with self._vad_lock: + peek = self._vad.peek_buffer() + if peek is None: + return False + audio, duration = peek + + if duration < 1.5: + return False + + use_word_ts = False + + try: + if use_word_ts: + result, asr_ms = self._run_asr( + audio, "interim", word_timestamps=use_word_ts + ) + else: + result, asr_ms = self._run_asr(audio, "interim") + except Exception as exc: + log.error(f"Interim ASR error: {exc}", exc_info=True) + self._emit_asr_error("interim", exc) + return False + + if asr_ms == 0 or result is None: + return False + + full_text = result["text"].strip() + if not full_text or not any(c.isalnum() for c in full_text): + return False + + full_text = self._strip_committed_overlap(full_text) + if not full_text: + return False + + split_start = time.perf_counter() + sentences = self._split_sentences(full_text, result["language"]) + split_ms = (time.perf_counter() - split_start) * 1000 + if len(sentences) <= 1: + return False + log.debug( + f"Interim split [{result['language']}] ({split_ms:.1f}ms): " + f"{len(sentences)} parts -> {sentences}" + ) + + complete = sentences[:-1] + committed_text = "" + for sent in complete: + committed_text += sent + + if not committed_text.strip(): + return False + + total_samples = len(audio) + if use_word_ts and result.get("words"): + words = result["words"] + committed_lower = committed_text.lower().rstrip() + char_pos = 0 + last_word_end = 0.0 + for word in words: + word_text = word["word"].strip() + idx = committed_lower.find(word_text.lower(), char_pos) + if idx >= 0: + char_pos = idx + len(word_text) + last_word_end = word["end"] + if char_pos >= len(committed_lower): + break + trim_samples = int(last_word_end * self._sample_rate) + else: + ratio = len(committed_text) / max(len(full_text), 1) + margin = int(0.3 * self._sample_rate) + trim_samples = int(ratio * total_samples) + margin + max_trim = total_samples - int(0.5 * self._sample_rate) + trim_samples = min(trim_samples, max(max_trim, 0)) + min_trim = int(0.3 * self._sample_rate) + if 0 < trim_samples < min_trim: + trim_samples = min(min_trim, total_samples // 2) + + actually_committed = False + for sent in complete: + text = sent.strip() + if not text: + continue + if self._is_short_utterance(text): + self._interim_pending += text + log.debug( + f"Interim short utterance buffered: '{text}', " + f"pending='{self._interim_pending}'" + ) + continue + + if self._interim_pending: + text = self._interim_pending + text + self._interim_pending = "" + + self._process_segment_text(text, result["language"], asr_ms) + actually_committed = True + + if not actually_committed: + return False + + if trim_samples > 0: + with self._vad_lock: + self._vad.trim_front(trim_samples) + + self._interim_committed_tail = ( + committed_text[-50:] if len(committed_text) > 50 else committed_text + ) + + self._interim_active = True + log.info( + f"Interim ASR: committed {len(complete)} sentence(s), " + f"trimmed {trim_samples / self._sample_rate:.2f}s" + ) + return True + + def _process_segment_text( + self, text: str, source_lang: str, asr_ms: float = 0.0 + ): + original_text = text.strip() + if not original_text or not any(c.isalnum() for c in original_text): + return + + if not self._language_allowed(source_lang, original_text): + return + + log.info(f"ASR [{source_lang}] ({asr_ms:.0f}ms, interim): {original_text}") + self._emit_asr_text(original_text, source_lang, asr_ms, interim=True) + + def _process_interim_final(self, speech_segment): + seg_len = len(speech_segment) / self._sample_rate + log.info(f"Interim final segment: {seg_len:.1f}s") + self._emit_speech_segment("interim_final", seg_len) + + try: + result, asr_ms = self._run_asr(speech_segment, "interim_final") + except Exception as exc: + log.error(f"Interim final ASR error: {exc}", exc_info=True) + self._emit_asr_error("interim_final", exc) + return + if asr_ms == 0: + return + + if result is None: + if self._interim_pending: + text = self._interim_pending + self._interim_pending = "" + lang = self._asr_language() + if lang == "auto": + lang = "unknown" + self._process_segment_text(text, lang) + return + + original_text = result["text"].strip() + original_text = self._strip_committed_overlap(original_text) + + if self._interim_pending: + original_text = self._interim_pending + original_text + self._interim_pending = "" + + if not original_text or not any(c.isalnum() for c in original_text): + return + + alnum_chars = sum(1 for c in original_text if c.isalnum()) + if seg_len >= 2.0 and alnum_chars <= 3: + log.debug( + f"Noise filter: {seg_len:.1f}s segment produced only " + f"'{original_text}', skipping" + ) + return + + self._process_segment_text(original_text, result["language"], asr_ms) + + def _drain_interim_duplicates(self): + while True: + try: + item = self._asr_queue.get_nowait() + except queue.Empty: + break + if item is None or item[0] != "interim": + self._asr_queue.put(item) + break + + def _run_asr(self, audio: np.ndarray, kind: str, **kwargs): + if not self._is_asr_ready(): + return None, 0.0 + return self._asr_runner(audio, kind, **kwargs) + + def _language_allowed(self, source_lang: str, text: str) -> bool: + asr_lang_setting = self._asr_language() + if asr_lang_setting != "auto" and source_lang != asr_lang_setting: + log.info( + f"Language filter: expected '{asr_lang_setting}' but got " + f"'{source_lang}', discarding: {text[:60]}" + ) + return False + return True + + def _is_asr_ready(self) -> bool: + try: + return self._asr_ready() + except Exception as exc: + log.warning(f"ASR readiness check failed: {exc}") + return False + + def _reset_interim_state(self): + self._interim_active = False + self._interim_pending = "" + self._last_interim_samples = 0 + self._last_interim_check_time = 0.0 + self._interim_committed_tail = "" + + def _emit_audio_level( + self, rms: float, vad_confidence: float, mic_rms: float | None = None + ): + if self._audio_level_callback is not None: + self._audio_level_callback(AudioLevelEvent(rms, vad_confidence, mic_rms)) + + def _emit_speech_segment(self, kind: str, audio_seconds: float): + if self._speech_segment_callback is not None: + self._speech_segment_callback(SpeechSegmentEvent(kind, audio_seconds)) + + def _emit_asr_text( + self, text: str, source_lang: str, asr_ms: float, interim: bool + ): + if self._asr_text_callback is not None: + self._asr_text_callback( + ASRTextEvent(text, source_lang, asr_ms, interim=interim) + ) + + def _emit_asr_error(self, kind: str, error: Exception): + if self._asr_error_callback is not None: + self._asr_error_callback(ASRErrorEvent(kind, error)) + + def _emit_stats(self, state: str): + if self._stats_callback is not None: + self._stats_callback( + PipelineStatsEvent(state, queue_size=self._asr_queue.qsize()) + ) From d45d8a85e08bd6bd79150f22848c4e39c04ed3ea Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 05:53:43 +0800 Subject: [PATCH 07/13] =?UTF-8?q?fix:=20openAI=20SDK=20=E8=A6=81=E6=B1=82?= =?UTF-8?q?=20apikey=20=E4=B8=8D=E4=B8=BA=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 666e64c..dbd79a0 100644 --- a/config.yaml +++ b/config.yaml @@ -42,7 +42,7 @@ asr: translation: # OpenAI-compatible API settings api_base: "http://127.0.0.1:1234/v1" - api_key: "" + api_key: "dummy" model: "hunyuan-mt-chimera-7b" # Target language for translation target_language: "zh" From a1632eb3e6fc97d36a14079b5c99093791c310e1 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sat, 20 Jun 2026 21:55:56 +0800 Subject: [PATCH 08/13] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20CrispASR=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B=E4=B8=8B=E8=BD=BD=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CrispASR 不再维护内置模型 profile,也不再将模型 key 回退到默认模型。 仅保留本地 .gguf/.bin 文件的校验、扫描和路径解析;ASR 切换时必须解析到有效本地文件,否则保持当前 worker。 控制面板移除 CrispASR 下载按钮和内置模型选项,下载对话框也不再处理 crispasr: 下载任务。 --- asr_service.py | 35 +----------- control_panel.py | 52 ++---------------- dialogs.py | 8 --- i18n/en.yaml | 4 +- i18n/zh.yaml | 4 +- main.py | 8 --- model_manager.py | 137 +++-------------------------------------------- 7 files changed, 15 insertions(+), 233 deletions(-) diff --git a/asr_service.py b/asr_service.py index 07f915e..c48010d 100644 --- a/asr_service.py +++ b/asr_service.py @@ -7,13 +7,10 @@ from asr_client import ASRClient, ASRWorkerError, ASRWorkerExited, ASRWorkerTimeout from model_manager import ( ASR_DISPLAY_NAMES, - CRISPASR_MODEL_PROFILES, DEFAULT_FUNASR_MODEL, MODELS_DIR, - crispasr_profile, funasr_display_name, funasr_supports_padding, - get_local_model_path, get_missing_models, is_asr_cached, local_crispasr_display_name, @@ -180,18 +177,13 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: if custom_crispasr_path: cache_model_key = custom_crispasr_path crispasr_model_path_value = custom_crispasr_path - elif crispasr_model in CRISPASR_MODEL_PROFILES: - cache_model_key = crispasr_model - crispasr_model_path_value = get_local_model_path( - "crispasr", hub=hub, funasr_model=crispasr_model - ) else: return self._invalid_plan( engine_type, device, hub, download_proxy, - f"CrispASR model is invalid or unavailable: {crispasr_model}; " + f"CrispASR model must be a local .gguf/.bin file: {crispasr_model}; " "keeping current ASR worker", ) @@ -247,15 +239,6 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: display_name = self._display_name( engine_type, model_size, model_path, funasr_model, crispasr_model ) - if engine_type == "crispasr": - custom_crispasr_path = resolve_custom_crispasr_model(crispasr_model) - profile = {} if custom_crispasr_path else crispasr_profile(crispasr_model) - if ( - profile.get("needs_punctuation") - and crispasr_punc_model in (None, "", "auto") - ): - crispasr_punc_model = "auto" - worker_config = { "engine_type": engine_type, "funasr_model": funasr_model, @@ -326,18 +309,6 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: missing_models=missing, ) - def complete_download(self, plan: ASRSwitchPlan) -> str | None: - if plan.engine_type != "crispasr": - return None - model_path = get_local_model_path( - "crispasr", hub=plan.hub, funasr_model=plan.cache_model_key - ) - if not model_path: - return "CrispASR model file was not found after download" - plan.worker_config["crispasr_model_path"] = model_path - plan.target_state["config"] = plan.worker_config - return None - def mark_download_cancelled(self): with self._asr_lock: self._asr_ready = self._asr is not None and self._asr.status == "ready" @@ -572,8 +543,4 @@ def _display_name( or Path(crispasr_model).name ) display_name = f"CrispASR {display_model}" - else: - display_name = ( - f"CrispASR {crispasr_profile(crispasr_model)['display_name']}" - ) return display_name diff --git a/control_panel.py b/control_panel.py index bf80608..0bff422 100644 --- a/control_panel.py +++ b/control_panel.py @@ -36,7 +36,6 @@ DEFAULT_FUNASR_MODEL, MODELS_DIR, _WHISPER_SIZES, - crispasr_model_options, dir_size, funasr_model_options, funasr_supports_padding, @@ -374,7 +373,7 @@ def _create_vad_tab(self): layout.addWidget(self._whisper_group) self._whisper_group.setVisible(engine_idx == 0) - self._crispasr_group = QGroupBox(t("group_download_crispasr")) + self._crispasr_group = QGroupBox(t("group_crispasr_model")) crispasr_layout = QHBoxLayout(self._crispasr_group) self._crispasr_model_combo = QComboBox() saved_crispasr_model = s.get("crispasr_model", "") @@ -386,9 +385,6 @@ def _create_vad_tab(self): self._crispasr_status = QLabel("") self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") crispasr_layout.addWidget(self._crispasr_status, 1) - self._crispasr_dl_btn = QPushButton(t("btn_download_crispasr")) - self._crispasr_dl_btn.clicked.connect(self._download_crispasr) - crispasr_layout.addWidget(self._crispasr_dl_btn) layout.addWidget(self._crispasr_group) self._crispasr_group.setVisible(engine_idx == 3) self._asr_engine.currentIndexChanged.connect( @@ -1174,10 +1170,7 @@ def _selected_whisper_model(self) -> str: def _populate_crispasr_models(self, saved_value: str): self._crispasr_model_combo.clear() - builtin_models = dict(crispasr_model_options()) self._crispasr_model_combo.addItem(t("crispasr_model_placeholder"), "") - for key, display_name in crispasr_model_options(): - self._crispasr_model_combo.addItem(display_name, key) local_prefix = t("crispasr_local_prefix") for item in list_local_crispasr_models(): @@ -1191,8 +1184,6 @@ def _populate_crispasr_models(self, saved_value: str): if not saved_value: selected = "" - elif saved_value in builtin_models: - selected = saved_value else: selected = resolve_custom_crispasr_model(saved_value) or saved_value idx = self._crispasr_model_combo.findData(selected) @@ -1295,54 +1286,21 @@ def _download_whisper(self): self._auto_save() def _update_crispasr_status(self): - from model_manager import is_asr_cached, crispasr_profile + from model_manager import is_asr_cached model_key = self._selected_crispasr_model() if not model_key: self._crispasr_status.setText(t("crispasr_select_model")) self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") - self._crispasr_dl_btn.setEnabled(False) return hub = self._current_settings.get("hub", "ms") cached = is_asr_cached("crispasr", model_key, hub) - if model_key not in dict(crispasr_model_options()): - if cached: - self._crispasr_status.setText(t("crispasr_local_ready")) - self._crispasr_status.setStyleSheet("color: #4a4; font-size: 11px;") - else: - self._crispasr_status.setText(t("crispasr_invalid_local")) - self._crispasr_status.setStyleSheet("color: #d66; font-size: 11px;") - self._crispasr_dl_btn.setEnabled(False) - return if cached: - self._crispasr_status.setText(t("crispasr_already_cached")) + self._crispasr_status.setText(t("crispasr_local_ready")) self._crispasr_status.setStyleSheet("color: #4a4; font-size: 11px;") - self._crispasr_dl_btn.setEnabled(False) else: - est = crispasr_profile(model_key).get("estimated_bytes", 0) - self._crispasr_status.setText(f"~{format_size(est)}") - self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") - self._crispasr_dl_btn.setEnabled(True) - - def _download_crispasr(self): - from model_manager import is_asr_cached, get_missing_models - - model_key = self._selected_crispasr_model() - if not model_key: - return - hub = self._current_settings.get("hub", "ms") - if is_asr_cached("crispasr", model_key, hub): - return - missing = get_missing_models("crispasr", model_key, hub) - missing = [m for m in missing if m["type"] != "silero-vad"] - if not missing: - return - from dialogs import ModelDownloadDialog - - dlg = ModelDownloadDialog(missing, hub=hub, parent=self) - if dlg.exec() == dlg.DialogCode.Accepted: - self._update_crispasr_status() - self._auto_save() + self._crispasr_status.setText(t("crispasr_invalid_local")) + self._crispasr_status.setStyleSheet("color: #d66; font-size: 11px;") # ── Model Management ── diff --git a/dialogs.py b/dialogs.py index a573a12..735bd7a 100644 --- a/dialogs.py +++ b/dialogs.py @@ -406,14 +406,6 @@ def _download_worker(self): hub=self._hub, proxy=self._proxy, ) - elif m["type"].startswith("crispasr:"): - model_key = m["type"].split(":", 1)[1] - download_asr( - "crispasr", - model_size=model_key, - hub=self._hub, - proxy=self._proxy, - ) elif m["type"].startswith("whisper-"): size = m["type"].replace("whisper-", "") download_asr( diff --git a/i18n/en.yaml b/i18n/en.yaml index 6ac52ec..f77781e 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -157,11 +157,9 @@ whisper_local_prefix: "Local" whisper_local_ready: "Local model" whisper_missing_local: "Missing" whisper_invalid_local: "Local model unavailable" -group_download_crispasr: "CrispASR Model" -btn_download_crispasr: "Download" +group_crispasr_model: "CrispASR Model" crispasr_model_placeholder: "Select CrispASR model" crispasr_select_model: "Select a model" -crispasr_already_cached: "Already cached" crispasr_local_prefix: "Local" crispasr_local_ready: "Local model" crispasr_missing_local: "Missing" diff --git a/i18n/zh.yaml b/i18n/zh.yaml index 0bf4709..093eb39 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -157,11 +157,9 @@ whisper_local_prefix: "本地" whisper_local_ready: "本地模型" whisper_missing_local: "缺失" whisper_invalid_local: "本地模型不可用" -group_download_crispasr: "CrispASR 模型" -btn_download_crispasr: "下载" +group_crispasr_model: "CrispASR 模型" crispasr_model_placeholder: "请选择 CrispASR 模型" crispasr_select_model: "请选择模型" -crispasr_already_cached: "已缓存" crispasr_local_prefix: "本地" crispasr_local_ready: "本地模型" crispasr_missing_local: "缺失" diff --git a/main.py b/main.py index b18a492..9e2f8dd 100644 --- a/main.py +++ b/main.py @@ -346,14 +346,6 @@ def _switch_asr_engine(self, engine_type: str): log.info(f"Download cancelled/failed: {plan.engine_type}") self._asr_service.mark_download_cancelled() return - error = self._asr_service.complete_download(plan) - if error: - QMessageBox.warning( - parent, - t("error_title"), - t("error_load_asr").format(error=error), - ) - return dlg = _ModelLoadDialog( t("loading_model").format(name=plan.display_name), parent=parent diff --git a/model_manager.py b/model_manager.py index 182bd5e..2a644e8 100644 --- a/model_manager.py +++ b/model_manager.py @@ -142,21 +142,6 @@ def asr_model_id( "crispasr": "CrispASR", } -CRISPASR_MODEL_PROFILES = { - "parakeet-tdt-0.6b-v3-q4_k": { - "display_name": "Parakeet TDT 0.6B v3 Q4_K", - "repo_id": "cstr/parakeet-tdt-0.6b-v3-GGUF", - "filename": "parakeet-tdt-0.6b-v3-q4_k.gguf", - "backend": "auto", - "estimated_bytes": 650_000_000, - "languages": "25 EU", - "output_style": "native_punctuation", - "needs_punctuation": False, - "sha256": None, - }, -} - -DEFAULT_CRISPASR_MODEL = "parakeet-tdt-0.6b-v3-q4_k" _CRISPASR_EXTS = {".gguf", ".bin"} _CRISPASR_MIN_BYTES = 1_000_000 @@ -171,7 +156,6 @@ def asr_model_id( "whisper-medium": 1_530_000_000, "whisper-large-v3": 3_100_000_000, "anime-whisper": 3_100_000_000, - "crispasr:parakeet-tdt-0.6b-v3-q4_k": 650_000_000, } _WHISPER_SIZES = ["tiny", "base", "small", "medium", "large-v3"] @@ -240,23 +224,6 @@ def funasr_model_id(model_key: str | None, hub: str = "ms") -> str: return profile["huggingface_id"] if hub == "hf" else profile["modelscope_id"] -def normalize_crispasr_model_key(model_key: str | None) -> str: - if model_key in CRISPASR_MODEL_PROFILES: - return model_key - return DEFAULT_CRISPASR_MODEL - - -def crispasr_profile(model_key: str | None) -> dict: - return CRISPASR_MODEL_PROFILES[normalize_crispasr_model_key(model_key)] - - -def crispasr_model_options() -> list[tuple[str, str]]: - return [ - (key, profile["display_name"]) - for key, profile in CRISPASR_MODEL_PROFILES.items() - ] - - def is_crispasr_model_file(path) -> bool: if not path: return False @@ -272,7 +239,7 @@ def is_crispasr_model_file(path) -> bool: def _custom_crispasr_path(value) -> Path | None: - if not value or value in CRISPASR_MODEL_PROFILES: + if not value: return None path = Path(str(value)).expanduser() if not path.is_absolute(): @@ -287,44 +254,6 @@ def resolve_custom_crispasr_model(value) -> str | None: return None -def _hf_repo_cache_dir(repo_id: str) -> Path: - org, name = repo_id.split("/", 1) - return MODELS_DIR / "huggingface" / "hub" / f"models--{org}--{name}" - - -def _hf_cached_file(repo_id: str, filename: str) -> Path | None: - snap_root = _hf_repo_cache_dir(repo_id) / "snapshots" - if not snap_root.exists(): - return None - try: - snapshots = sorted( - (p for p in snap_root.iterdir() if p.is_dir()), - key=lambda p: p.stat().st_mtime, - reverse=True, - ) - except (OSError, PermissionError): - return None - for snap in snapshots: - path = snap / filename - if is_crispasr_model_file(path): - return path - return None - - -def _crispasr_legacy_model_path(model_key: str | None) -> Path: - profile = crispasr_profile(model_key) - return MODELS_DIR / "crispasr" / profile["filename"] - - -def crispasr_model_path(model_key: str | None, hub: str = "hf") -> str: - profile = crispasr_profile(model_key) - cached = _hf_cached_file(profile["repo_id"], profile["filename"]) - if cached: - return str(cached.resolve()) - legacy = _crispasr_legacy_model_path(model_key) - return str(legacy.resolve()) - - def _custom_whisper_path(value) -> Path | None: if not value or value in _WHISPER_SIZES: return None @@ -358,15 +287,6 @@ def _is_builtin_whisper_cache(path: Path) -> bool: return any(f"models--Systran--faster-whisper-{s}" in parts for s in _WHISPER_SIZES) -def _is_builtin_crispasr_cache(path: Path) -> bool: - parts = set(path.parts) - for profile in CRISPASR_MODEL_PROFILES.values(): - org, name = profile["repo_id"].split("/", 1) - if f"models--{org}--{name}" in parts: - return True - return False - - def _is_hf_hub_cache(path: Path) -> bool: parts = path.parts marker = ("huggingface", "hub") @@ -453,8 +373,6 @@ def list_local_crispasr_models() -> list[dict]: return [] for path in files: - if _is_builtin_crispasr_cache(path): - continue if path.name == "model.bin" and is_faster_whisper_model_dir(path.parent): continue if not is_crispasr_model_file(path): @@ -554,15 +472,7 @@ def _hf_repo_complete(org: str, name: str, min_bytes: int = 50_000_000) -> bool: def is_asr_cached(engine_type, model_size="medium", hub="ms") -> bool: if engine_type == "crispasr": - custom = resolve_custom_crispasr_model(model_size) - if custom: - return True - if model_size not in CRISPASR_MODEL_PROFILES: - return False - profile = crispasr_profile(model_size) - if _hf_cached_file(profile["repo_id"], profile["filename"]): - return True - return is_crispasr_model_file(_crispasr_legacy_model_path(model_size)) + return resolve_custom_crispasr_model(model_size) is not None if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -624,7 +534,7 @@ def get_missing_models(engine, model_size, hub) -> list: if not is_asr_cached(engine, model_size, hub): if engine == "whisper" and model_size not in _WHISPER_SIZES: return missing - if engine == "crispasr" and model_size not in CRISPASR_MODEL_PROFILES: + if engine == "crispasr": return missing if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -640,11 +550,6 @@ def get_missing_models(engine, model_size, hub) -> list: key = engine if engine != "whisper" else f"whisper-{model_size}" display = f"Whisper {model_size}" estimated_bytes = _MODEL_SIZE_BYTES.get(key, 0) - elif engine == "crispasr": - profile = crispasr_profile(model_size) - key = f"crispasr:{normalize_crispasr_model_key(model_size)}" - display = f"CrispASR {profile['display_name']}" - estimated_bytes = profile["estimated_bytes"] else: key = engine display = ASR_DISPLAY_NAMES.get(engine, engine) @@ -666,17 +571,7 @@ def get_local_model_path(engine_type, hub="ms", funasr_model: str | None = None) """ if engine_type == "crispasr": model_value = funasr_model - custom = resolve_custom_crispasr_model(model_value) - if custom: - return custom - if model_value not in CRISPASR_MODEL_PROFILES: - return None - profile = crispasr_profile(model_value) - path = _hf_cached_file(profile["repo_id"], profile["filename"]) - if path: - return str(path.resolve()) - legacy = _crispasr_legacy_model_path(model_value) - return str(legacy.resolve()) if is_crispasr_model_file(legacy) else None + return resolve_custom_crispasr_model(model_value) if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -771,22 +666,7 @@ def download_asr(engine, model_size="medium", hub="ms", proxy="system"): ms_cache = os.path.join(resolved, "modelscope") hf_cache = os.path.join(resolved, "huggingface", "hub") with _proxy_env(proxy): - if engine == "crispasr": - if model_size not in CRISPASR_MODEL_PROFILES: - raise ValueError(f"Invalid CrispASR model: {model_size}") - from huggingface_hub import hf_hub_download - - profile = crispasr_profile(model_size) - log.info( - f"Downloading {profile['repo_id']}:{profile['filename']} " - "from HuggingFace..." - ) - hf_hub_download( - repo_id=profile["repo_id"], - filename=profile["filename"], - cache_dir=hf_cache, - ) - elif engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: + if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine] if engine in FUNASR_LEGACY_ENGINE_ALIASES @@ -819,6 +699,8 @@ def download_asr(engine, model_size="medium", hub="ms", proxy="system"): model_id = f"Systran/faster-whisper-{model_size}" log.info(f"Downloading {model_id} from HuggingFace...") snapshot_download(repo_id=model_id, cache_dir=hf_cache) + else: + raise ValueError(f"Unsupported ASR download engine: {engine}") log.info(f"ASR model downloaded: {engine}") @@ -871,11 +753,6 @@ def get_cache_entries(): if hf_path.exists(): entries.append((f"{name} (HuggingFace)", hf_path)) - for profile in CRISPASR_MODEL_PROFILES.values(): - repo_dir = _hf_repo_cache_dir(profile["repo_id"]) - if _hf_cached_file(profile["repo_id"], profile["filename"]): - entries.append((f"CrispASR {profile['display_name']} (HuggingFace)", repo_dir)) - for size in _WHISPER_SIZES: hf_path = hf_base / f"models--Systran--faster-whisper-{size}" if hf_path.exists() and is_asr_cached("whisper", size, "hf"): From 08fe42f2302bf6bca0bc1d35e940b36e4b4049ca Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sun, 21 Jun 2026 01:31:27 +0800 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20sherpa-onnx?= =?UTF-8?q?=20ASR=20=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 sherpa-onnx ASR 集成入口,并接入 ASR service、worker、主程序、控制面板和模型管理流程。 补充默认配置、设置存储、安装脚本与发布脚本,覆盖依赖安装、模型选择和打包所需配置。 同步 README、CLAUDE、中英文文案和变更日志,说明新引擎相关使用与发布信息。 --- CLAUDE.md | 2 + README.md | 15 +- README_zh.md | 15 +- asr_service.py | 130 ++++++++++++++- asr_sherpa_onnx.py | 368 +++++++++++++++++++++++++++++++++++++++++++ asr_worker.py | 79 +++++++++- build_release.ps1 | 5 + config.yaml | 12 +- control_panel.py | 251 ++++++++++++++++++++++++----- i18n/CHANGELOG_en.md | 7 + i18n/CHANGELOG_zh.md | 7 + i18n/en.yaml | 13 ++ i18n/zh.yaml | 13 ++ install.ps1 | 36 ++++- main.py | 20 +++ model_manager.py | 299 ++++++++++++++++++++++++++++++++++- settings_store.py | 19 +++ 17 files changed, 1240 insertions(+), 51 deletions(-) create mode 100644 asr_sherpa_onnx.py diff --git a/CLAUDE.md b/CLAUDE.md index d14fc67..5695a49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,7 @@ main.py (LiveTranslateApp) |-- asr_funasr_nano.py FunASR Nano backend |-- asr_anime_whisper.py Anime-Whisper backend (litagin/anime-whisper, ja anime/galgame) |-- asr_crispasr.py CrispASR ggml runtime backend (GGUF/bin single-file models) + |-- asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer backend (local ONNX model dirs) |-- translator.py OpenAI-compatible API client, streaming, JSON schema, context history |-- subtitle_overlay.py PyQt6 transparent overlay (2-row header: controls + model/lang combos) |-- subtitle_window.py Standalone subtitle window for OBS capture (outlined text, animations) @@ -171,6 +172,7 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr - FunASR `disable_pbar=True` required in all `generate()` calls — tqdm crashes in GUI process when flushing stderr - ASR engine lifecycle: the GUI process never instantiates `ASREngine`, `FunASREngine`, or `AnimeWhisperEngine` directly. It owns an `ASRClient`; each worker process owns one concrete backend/model. Engine/model/device changes shut down the current worker first, then start a new worker. On target load failure, the saved previous worker config is used to restore ASR. - CrispASR is treated as a ggml C++ runtime hub with GGUF/bin single-file weights. The GUI process never imports `crispasr`; `asr_crispasr.py` imports the Python binding only inside the ASR worker process and adapts results to the common ASR dict. +- sherpa-onnx is treated as a local ONNX ASR runtime. The GUI process never imports `sherpa_onnx`; `asr_sherpa_onnx.py` imports it lazily inside the ASR worker after any CUDA device mapping is finalized. Offline families use `OfflineRecognizer`; `online_transducer` uses `OnlineRecognizer` as a VAD segment wrapper, not true partial streaming. - CrispASR packaging has two parts that must stay aligned: `pyproject.toml` installs the pure-Python binding from the CrispASR release tag via `uv sync`, while `install.ps1` and the portable `bootstrap.ps1` download the matching prebuilt Windows `libcrispasr` DLL runtime from GitHub Releases. Do not replace this with plain `crispasr` from PyPI; that package name may not exist in the configured registry and the binding alone does not include `crispasr.dll`. - CrispASR native runtime install prefers `libcrispasr-windows-x86_64-cuda.tar.gz` on NVIDIA systems and falls back to `libcrispasr-windows-x86_64.tar.gz`. DLLs are copied next to the installed `crispasr` package so the worker can load them without a global PATH edit. - Whisper (ctranslate2) only accepts `device="cuda"` not `"cuda:0"`; device index passed via `device_index` param. Parsed from combo text like `"cuda:0 (RTX 4090)"` in `_switch_asr_engine` diff --git a/README.md b/README.md index 3f7cdb4..a51aa0e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Works with any system audio — videos, livestreams, voice chat. No player modif ## Features - **Real-time pipeline**: System audio → VAD → ASR → LLM translation → overlay -- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper, CrispASR +- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper, CrispASR, sherpa-onnx - **Any OpenAI-compatible API**: DeepSeek, Grok, Qwen, GPT, Ollama, vLLM, etc. - **Streaming translation display**: Real-time character-by-character translation output - **Per-model settings**: Streaming, structured output (JSON), context history, disable thinking @@ -81,6 +81,7 @@ uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # Dependencies uv sync --locked --inexact --no-install-package torch --no-install-package torchaudio uv pip install funasr --no-deps +uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" # Launch .venv\Scripts\python.exe main.py @@ -96,6 +97,16 @@ uv pip install funasr --no-deps 2. Silero VAD + SenseVoice models download automatically (~1GB) 3. Main UI appears when ready +## sherpa-onnx Models + +LiveTranslate supports sherpa-onnx local ONNX models through Python `OfflineRecognizer` and `OnlineRecognizer` APIs. Online models are currently decoded as a VAD-segment wrapper, not as true partial streaming ASR. `install.ps1` installs the CPU wheel by default. CUDA requires replacing it with a CUDA wheel, for example: + +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 -SherpaOnnxRuntime cuda12 +``` + +Download sherpa-onnx ASR model archives from the official sherpa-onnx releases, extract them anywhere under `models/`, then open Settings → VAD/ASR, choose `sherpa-onnx (ONNX)`, click Refresh, and select the local model directory. Online transducer scans accept `encoder.onnx`/`decoder.onnx`/`joiner.onnx` and int8 variants such as `encoder.int8.onnx`/`decoder.int8.onnx`/`joiner.int8.onnx`. PR #3671 Nemotron packages are published with names like `sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-560ms-int8-2026-06-11`; unofficial snapshots must still have ONNX files accepted by the installed sherpa-onnx/ONNX Runtime version. The `onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4` layout is not treated as a sherpa-onnx model in this path. + ## Translation API Settings → Translation tab: @@ -126,6 +137,7 @@ main.py Entry point & pipeline ├── asr_funasr_nano.py FunASR Nano backend ├── asr_anime_whisper.py Anime-Whisper backend (ja anime/galgame) ├── asr_crispasr.py CrispASR ggml runtime backend +├── asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer backend ├── translator.py OpenAI-compatible client (streaming, JSON schema, context) ├── model_manager.py Model download & cache ├── subtitle_overlay.py PyQt6 overlay @@ -140,6 +152,7 @@ main.py Entry point & pipeline - [FunASR](https://github.com/modelscope/FunASR) — SenseVoice / Fun-ASR-Nano - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — Japanese anime/galgame ASR - CrispASR — ggml C++ ASR runtime hub with GGUF/bin single-file models, used through its Python binding in the ASR worker +- [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — ONNX ASR runtime used through `OfflineRecognizer` and segment-wrapped `OnlineRecognizer` - [Silero VAD](https://github.com/snakers4/silero-vad) — Voice activity detection ## Star History diff --git a/README_zh.md b/README_zh.md index b03f3c5..6ce6a6d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -21,7 +21,7 @@ Windows 实时音频翻译工具。捕获系统音频(WASAPI loopback)和可 ## 功能特性 - **实时翻译管线**:系统音频 → VAD → ASR → LLM 翻译 → 字幕显示 -- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper、CrispASR +- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper、CrispASR、sherpa-onnx - **兼容任意 OpenAI 格式 API**:DeepSeek、Grok、Qwen、GPT、Ollama、vLLM 等 - **流式翻译显示**:翻译结果逐字实时显示 - **模型独立配置**:流式传输、结构化输出(JSON)、上下文历史、禁用思考 @@ -81,6 +81,7 @@ uv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # 依赖 uv sync --locked --inexact --no-install-package torch --no-install-package torchaudio uv pip install funasr --no-deps +uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" # 启动 .venv\Scripts\python.exe main.py @@ -96,6 +97,16 @@ uv pip install funasr --no-deps 2. 自动下载 Silero VAD + SenseVoice 模型(约 1GB) 3. 下载完成后进入主界面 +## sherpa-onnx 模型 + +LiveTranslate 通过 Python `OfflineRecognizer` 和 `OnlineRecognizer` API 接入 sherpa-onnx 本地 ONNX 模型。Online 模型当前是对 VAD 切段做整段 segment-wrapper 解码,不是真正逐 chunk partial streaming ASR。`install.ps1` 默认安装 CPU wheel;CUDA 版需要用 CUDA wheel 替换,例如: + +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 -SherpaOnnxRuntime cuda12 +``` + +从 sherpa-onnx 官方 releases 下载 ASR 模型压缩包,解压到 `models/` 下任意子目录;然后在 设置 → VAD/ASR 中选择 `sherpa-onnx (ONNX)`,点击刷新并选择本地模型目录。Online transducer 扫描支持 `encoder.onnx`/`decoder.onnx`/`joiner.onnx`,也支持 `encoder.int8.onnx`/`decoder.int8.onnx`/`joiner.int8.onnx`。PR #3671 对应的 Nemotron 包名类似 `sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-560ms-int8-2026-06-11`;非官方 snapshot 的 ONNX 文件仍必须能被当前安装的 sherpa-onnx/ONNX Runtime 接受。`onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4` 这类结构不属于此 sherpa-onnx 路线。 + ## 配置翻译 API 设置 → 翻译标签页: @@ -126,6 +137,7 @@ main.py 主入口,管线编排 ├── asr_funasr_nano.py FunASR Nano 后端 ├── asr_anime_whisper.py Anime-Whisper 后端 (日语动画/Galgame) ├── asr_crispasr.py CrispASR ggml runtime 后端 +├── asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer 后端 ├── translator.py OpenAI 兼容翻译客户端 (流式/JSON/上下文) ├── model_manager.py 模型下载与缓存管理 ├── subtitle_overlay.py PyQt6 透明悬浮窗 @@ -140,6 +152,7 @@ main.py 主入口,管线编排 - [FunASR](https://github.com/modelscope/FunASR) — SenseVoice / Fun-ASR-Nano - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — 日语动画/Galgame 专用 ASR - CrispASR — ggml C++ ASR runtime hub,使用 GGUF/bin 单文件模型,LiveTranslate 通过 ASR worker 内的 Python binding 调用 +- [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — 通过 `OfflineRecognizer` 和 segment-wrapper `OnlineRecognizer` 调用的 ONNX ASR runtime - [Silero VAD](https://github.com/snakers4/silero-vad) — 语音活动检测 ## Star History diff --git a/asr_service.py b/asr_service.py index c48010d..86ccfdc 100644 --- a/asr_service.py +++ b/asr_service.py @@ -9,12 +9,15 @@ ASR_DISPLAY_NAMES, DEFAULT_FUNASR_MODEL, MODELS_DIR, + detect_sherpa_onnx_model_dir, funasr_display_name, funasr_supports_padding, get_missing_models, + get_sherpa_onnx_model_path, is_asr_cached, local_crispasr_display_name, local_faster_whisper_display_name, + local_sherpa_onnx_display_name, normalize_asr_engine_selection, normalize_funasr_model_key, resolve_custom_crispasr_model, @@ -82,6 +85,9 @@ def __init__( config["asr"].get("funasr_model", DEFAULT_FUNASR_MODEL) ) self._crispasr_model_key = str(config["asr"].get("crispasr_model", "") or "") + self._sherpa_onnx_model_path = str( + config["asr"].get("sherpa_onnx_model", "") or "" + ) self._asr_lock = threading.RLock() @property @@ -157,6 +163,54 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: self._config["asr"].get("crispasr_unified_memory", True), ) ) + sherpa_onnx_model = str( + settings.get( + "sherpa_onnx_model", + self._config["asr"].get( + "sherpa_onnx_model", self._sherpa_onnx_model_path + ), + ) + or "" + ) + sherpa_onnx_provider = str( + settings.get( + "sherpa_onnx_provider", + self._config["asr"].get("sherpa_onnx_provider", "auto"), + ) + or "auto" + ).lower() + sherpa_onnx_num_threads = int( + settings.get( + "sherpa_onnx_num_threads", + self._config["asr"].get("sherpa_onnx_num_threads", 2), + ) + or 2 + ) + sherpa_onnx_decoding_method = str( + settings.get( + "sherpa_onnx_decoding_method", + self._config["asr"].get( + "sherpa_onnx_decoding_method", "greedy_search" + ), + ) + or "greedy_search" + ) + sherpa_onnx_tail_padding_seconds = float( + settings.get( + "sherpa_onnx_tail_padding_seconds", + self._config["asr"].get("sherpa_onnx_tail_padding_seconds", 0.5), + ) + or 0.0 + ) + sherpa_onnx_left_padding_seconds = float( + settings.get( + "sherpa_onnx_left_padding_seconds", + self._config["asr"].get("sherpa_onnx_left_padding_seconds", 0.3), + ) + or 0.0 + ) + sherpa_onnx_model_path = None + sherpa_onnx_model_info = None if engine_type == "whisper": model_path = resolve_custom_whisper_model(model_size) @@ -186,6 +240,38 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: f"CrispASR model must be a local .gguf/.bin file: {crispasr_model}; " "keeping current ASR worker", ) + elif engine_type == "sherpa-onnx": + if not sherpa_onnx_model: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + "sherpa-onnx model is not selected; keeping current ASR worker", + ) + sherpa_onnx_model_path = get_sherpa_onnx_model_path(sherpa_onnx_model) + if not sherpa_onnx_model_path: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"sherpa-onnx model directory is unavailable or unrecognized: " + f"{sherpa_onnx_model}; keeping current ASR worker", + ) + sherpa_onnx_model_info = detect_sherpa_onnx_model_dir( + sherpa_onnx_model_path + ) + if not sherpa_onnx_model_info: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"sherpa-onnx model directory is unavailable or unrecognized: " + f"{sherpa_onnx_model_path}; keeping current ASR worker", + ) + cache_model_key = sherpa_onnx_model_path compute = self._config["asr"]["compute_type"] if engine_type == "whisper": @@ -201,6 +287,16 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: crispasr_punc_model, crispasr_unified_memory, ) + elif engine_type == "sherpa-onnx": + signature_model = ( + sherpa_onnx_model_path, + sherpa_onnx_model_info["family"], + sherpa_onnx_provider, + sherpa_onnx_num_threads, + sherpa_onnx_decoding_method, + sherpa_onnx_left_padding_seconds, + sherpa_onnx_tail_padding_seconds, + ) else: signature_model = engine_type language = settings.get( @@ -237,7 +333,12 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: cached = is_asr_cached(engine_type, cache_model_key, hub) display_name = self._display_name( - engine_type, model_size, model_path, funasr_model, crispasr_model + engine_type, + model_size, + model_path, + funasr_model, + crispasr_model, + sherpa_onnx_model_path, ) worker_config = { "engine_type": engine_type, @@ -275,6 +376,19 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: "crispasr_unified_memory": crispasr_unified_memory, } ) + elif engine_type == "sherpa-onnx": + worker_config.update( + { + "sherpa_onnx_model": sherpa_onnx_model_path, + "sherpa_onnx_model_path": sherpa_onnx_model_path, + "sherpa_onnx_model_info": sherpa_onnx_model_info, + "sherpa_onnx_provider": sherpa_onnx_provider, + "sherpa_onnx_num_threads": sherpa_onnx_num_threads, + "sherpa_onnx_decoding_method": sherpa_onnx_decoding_method, + "sherpa_onnx_left_padding_seconds": sherpa_onnx_left_padding_seconds, + "sherpa_onnx_tail_padding_seconds": sherpa_onnx_tail_padding_seconds, + } + ) target_state = { "type": engine_type, "signature": signature, @@ -288,6 +402,9 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: "crispasr_model_key": crispasr_model if engine_type == "crispasr" else self._crispasr_model_key, + "sherpa_onnx_model_path": sherpa_onnx_model_path + if engine_type == "sherpa-onnx" + else self._sherpa_onnx_model_path, "config": worker_config, "display_name": display_name, } @@ -324,6 +441,7 @@ def switch_worker(self, plan: ASRSwitchPlan) -> ASRSwitchResult: "funasr_model_key": self._funasr_model_key, "whisper_model_size": self._whisper_model_size, "crispasr_model_key": self._crispasr_model_key, + "sherpa_onnx_model_path": self._sherpa_onnx_model_path, "config": old_config, "display_name": (old_config or {}).get("display_name"), } @@ -514,6 +632,9 @@ def _activate_asr(self, client: ASRClient, state: dict): self._funasr_model_key = state["funasr_model_key"] self._whisper_model_size = state["whisper_model_size"] self._crispasr_model_key = state["crispasr_model_key"] + self._sherpa_onnx_model_path = state.get( + "sherpa_onnx_model_path", self._sherpa_onnx_model_path + ) self._asr_ready = True self._asr_error_count = 0 @@ -524,6 +645,7 @@ def _display_name( model_path: str | None, funasr_model: str, crispasr_model: str, + sherpa_onnx_model_path: str | None = None, ) -> str: display_name = ASR_DISPLAY_NAMES.get(engine_type, engine_type) if engine_type == "whisper": @@ -543,4 +665,10 @@ def _display_name( or Path(crispasr_model).name ) display_name = f"CrispASR {display_model}" + elif engine_type == "sherpa-onnx" and sherpa_onnx_model_path: + display_model = ( + local_sherpa_onnx_display_name(sherpa_onnx_model_path) + or Path(sherpa_onnx_model_path).name + ) + display_name = f"sherpa-onnx {display_model}" return display_name diff --git a/asr_sherpa_onnx.py b/asr_sherpa_onnx.py new file mode 100644 index 0000000..8d4197c --- /dev/null +++ b/asr_sherpa_onnx.py @@ -0,0 +1,368 @@ +import gc +import importlib.metadata +import inspect +import logging +import re +from pathlib import Path +from typing import Any + +import numpy as np + +from translator import LANGUAGE_DISPLAY + +log = logging.getLogger("LiveTranslate.SherpaOnnx") + +SENSE_VOICE_LANGUAGES = {"auto", "zh", "en", "ja", "ko", "yue"} +LANGUAGE_NAMES = {**LANGUAGE_DISPLAY, "auto": "auto", "yue": "Cantonese"} + + +def sherpa_onnx_cuda_wheel_available() -> bool: + try: + version = importlib.metadata.version("sherpa-onnx") + except importlib.metadata.PackageNotFoundError: + return False + return "+cuda" in version.lower() + + +def _supports_kwarg(fn, name: str) -> bool: + try: + params = inspect.signature(fn).parameters + except (TypeError, ValueError): + return True + return name in params or any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values() + ) + + +def _call_factory(factory, provider: str, kwargs: dict): + kwargs = {key: value for key, value in kwargs.items() if value is not None} + if _supports_kwarg(factory, "provider"): + kwargs["provider"] = provider + elif provider == "cuda": + raise RuntimeError( + "Installed sherpa-onnx recognizer API does not accept provider=; " + "upgrade sherpa-onnx or use CPU provider" + ) + else: + log.warning("sherpa-onnx API has no provider parameter; loading CPU path") + + try: + params = inspect.signature(factory).parameters + except (TypeError, ValueError): + params = {} + if params and not any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()): + kwargs = {key: value for key, value in kwargs.items() if key in params} + return factory(**kwargs) + + +def _sense_voice_language(language: str) -> str: + language = str(language or "auto").lower() + return language if language in SENSE_VOICE_LANGUAGES else "auto" + + +def _whisper_language(language: str) -> str: + language = str(language or "auto").lower() + if language == "auto": + return "" + if language == "ja": + return "jp" + return language + + +class SherpaOnnxEngine: + """sherpa-onnx recognizer adapter for VAD-segmented audio.""" + + def __init__( + self, + model_path: str, + model_info: dict, + provider: str = "cpu", + num_threads: int = 2, + language: str = "auto", + decoding_method: str = "greedy_search", + left_padding_seconds: float = 0.3, + tail_padding_seconds: float = 0.5, + ): + self.model_path = str(Path(model_path).resolve()) + self.model_info = dict(model_info or {}) + self.family = self.model_info.get("family") + self.provider = str(provider or "cpu").lower() + self.num_threads = int(num_threads or 2) + self.language = language or "auto" + self.decoding_method = decoding_method or "greedy_search" + self.left_padding_seconds = max(0.0, float(left_padding_seconds or 0.0)) + self.tail_padding_seconds = max(0.0, float(tail_padding_seconds or 0.0)) + self.sample_rate = int(self.model_info.get("sample_rate") or 16000) + self._recognizer = None + + if self.provider not in ("cpu", "cuda"): + raise ValueError(f"Unsupported sherpa-onnx provider: {self.provider}") + if self.provider == "cuda" and not sherpa_onnx_cuda_wheel_available(): + raise RuntimeError( + "sherpa-onnx CUDA provider selected, but the installed package is " + "not a CUDA wheel. Install sherpa-onnx==...+cuda or select CPU." + ) + + # Imported only in the ASR worker after CUDA_VISIBLE_DEVICES is finalized. + import sherpa_onnx + + self._sherpa_onnx = sherpa_onnx + self._load_recognizer() + log.info( + "sherpa-onnx loaded: " + f"family={self.family}, model={self.model_path}, provider={self.provider}, " + f"threads={self.num_threads}" + ) + + def _load_recognizer(self): + if self.family == "online_transducer": + self._load_online_transducer() + return + + if self.family == "sense_voice": + factory = self._sherpa_onnx.OfflineRecognizer.from_sense_voice + self._recognizer = _call_factory( + factory, + self.provider, + { + "model": self.model_info["model_file"], + "tokens": self.model_info["tokens_file"], + "num_threads": self.num_threads, + "language": _sense_voice_language(self.language), + "use_itn": True, + "debug": False, + }, + ) + return + + if self.family == "paraformer": + factory = self._sherpa_onnx.OfflineRecognizer.from_paraformer + self._recognizer = _call_factory( + factory, + self.provider, + { + "paraformer": self.model_info["model_file"], + "model": self.model_info["model_file"], + "tokens": self.model_info["tokens_file"], + "num_threads": self.num_threads, + "sample_rate": self.sample_rate, + "feature_dim": int(self.model_info.get("feature_dim") or 80), + "decoding_method": self.decoding_method, + "debug": False, + }, + ) + return + + if self.family == "moonshine": + factory = self._sherpa_onnx.OfflineRecognizer.from_moonshine + self._recognizer = _call_factory( + factory, + self.provider, + { + "preprocessor": self.model_info["preprocessor_file"], + "preprocess": self.model_info["preprocessor_file"], + "encoder": self.model_info["encoder_file"], + "uncached_decoder": self.model_info["uncached_decoder_file"], + "cached_decoder": self.model_info["cached_decoder_file"], + "tokens": self.model_info["tokens_file"], + "num_threads": self.num_threads, + "decoding_method": self.decoding_method, + "debug": False, + }, + ) + return + + if self.family == "whisper": + factory = self._sherpa_onnx.OfflineRecognizer.from_whisper + self._recognizer = _call_factory( + factory, + self.provider, + { + "encoder": self.model_info["encoder_file"], + "decoder": self.model_info["decoder_file"], + "tokens": self.model_info["tokens_file"], + "num_threads": self.num_threads, + "decoding_method": self.decoding_method, + "language": _whisper_language(self.language), + "debug": False, + }, + ) + return + + raise ValueError(f"Unsupported sherpa-onnx model family: {self.family}") + + def _load_online_transducer(self): + factory = getattr(self._sherpa_onnx.OnlineRecognizer, "from_transducer", None) + if factory is None: + raise RuntimeError( + "Installed sherpa-onnx OnlineRecognizer API does not provide " + "from_transducer(); upgrade sherpa-onnx to use online transducer models" + ) + + self._recognizer = _call_factory( + factory, + self.provider, + { + "tokens": self.model_info["tokens_file"], + "encoder": self.model_info["encoder_file"], + "decoder": self.model_info["decoder_file"], + "joiner": self.model_info["joiner_file"], + "num_threads": self.num_threads, + "sample_rate": self.sample_rate, + "feature_dim": int(self.model_info.get("feature_dim") or 80), + "decoding_method": self.decoding_method, + "model_type": self.model_info.get("model_type"), + "modeling_unit": self.model_info.get("modeling_unit"), + "bpe_vocab": self.model_info.get("bpe_vocab"), + "debug": False, + }, + ) + + def set_language(self, language: str): + language = language or "auto" + if language == self.language: + return + old = self.language + self.language = language + if self.family in ("sense_voice", "whisper"): + self._load_recognizer() + log.info(f"sherpa-onnx language: {old} -> {self.language}") + + def unload(self): + self._recognizer = None + gc.collect() + + def transcribe(self, audio: np.ndarray, word_timestamps: bool = False) -> dict | None: + if audio.size == 0: + return None + if audio.dtype != np.float32 or not audio.flags["C_CONTIGUOUS"]: + audio = np.ascontiguousarray(audio, dtype=np.float32) + + stream = self._recognizer.create_stream() + if self.family == "online_transducer": + result = self._decode_online_segment(stream, audio) + else: + stream.accept_waveform(self.sample_rate, audio) + self._recognizer.decode_stream(stream) + result = getattr(stream, "result", None) + text = self._result_text(result).strip() + language = self._result_language(result, text) + text = self._strip_sense_voice_tags(text) + if not text: + return None + + normalized = { + "text": text, + "language": language, + "language_name": LANGUAGE_NAMES.get(language, language), + } + if word_timestamps: + words = self._extract_words(result) + if words: + normalized["words"] = words + return normalized + + def _decode_online_segment(self, stream, audio: np.ndarray): + set_option = getattr(stream, "set_option", None) + if callable(set_option): + language = str(self.language or "auto").strip() + if language: + set_option("language", language) + + if self.left_padding_seconds > 0: + pad = np.zeros( + int(round(self.sample_rate * self.left_padding_seconds)), + dtype=np.float32, + ) + if pad.size: + audio = np.concatenate((pad, audio)) + + if self.tail_padding_seconds > 0: + pad = np.zeros( + int(round(self.sample_rate * self.tail_padding_seconds)), + dtype=np.float32, + ) + if pad.size: + audio = np.concatenate((audio, pad)) + + stream.accept_waveform(self.sample_rate, audio) + input_finished = getattr(stream, "input_finished", None) + if callable(input_finished): + input_finished() + + max_steps = max(100, int((audio.size / max(self.sample_rate, 1) + 2.0) * 100)) + steps = 0 + while self._recognizer.is_ready(stream) and steps < max_steps: + self._recognizer.decode_stream(stream) + steps += 1 + if steps >= max_steps: + log.warning("sherpa-onnx online decode reached max decode steps") + + get_result_all = getattr(self._recognizer, "get_result_all", None) + if callable(get_result_all): + return get_result_all(stream) + get_result = getattr(self._recognizer, "get_result", None) + if callable(get_result): + return get_result(stream) + return getattr(stream, "result", None) + + def _result_text(self, result: Any) -> str: + if result is None: + return "" + if isinstance(result, str): + return result + text = getattr(result, "text", None) + if text is None and isinstance(result, dict): + text = result.get("text") + return str(text or "") + + def _result_language(self, result: Any, text: str) -> str: + for attr in ("language", "lang"): + value = getattr(result, attr, None) + if value: + return self._normalize_language(str(value)) + if isinstance(result, dict) and result.get(attr): + return self._normalize_language(str(result[attr])) + match = re.search(r"<\|([a-z]{2,3})\|>", text or "", re.IGNORECASE) + if match: + return self._normalize_language(match.group(1)) + return _sense_voice_language(self.language) if self.family == "sense_voice" else ( + self.language if self.language and self.language != "auto" else "auto" + ) + + def _normalize_language(self, language: str) -> str: + language = language.strip().lower() + return "ja" if language == "jp" else language + + def _strip_sense_voice_tags(self, text: str) -> str: + return re.sub(r"<\|[^|]+?\|>", "", text or "").strip() + + def _extract_words(self, result: Any) -> list[dict]: + words = [] + raw_words = getattr(result, "words", None) + tokens = getattr(result, "tokens", None) + timestamps = getattr(result, "timestamps", None) + if isinstance(result, dict): + raw_words = raw_words or result.get("words") + tokens = tokens or result.get("tokens") + timestamps = timestamps or result.get("timestamps") + + if raw_words: + for word in raw_words: + data = word if isinstance(word, dict) else vars(word) + text = data.get("word") or data.get("text") + if not text: + continue + words.append( + { + "word": str(text), + "start": float(data.get("start") or 0.0), + "end": float(data.get("end") or 0.0), + } + ) + return words + + if tokens and timestamps and len(tokens) == len(timestamps): + for token, start in zip(tokens, timestamps, strict=False): + words.append({"word": str(token), "start": float(start), "end": float(start)}) + return words diff --git a/asr_worker.py b/asr_worker.py index c06f5d7..72ef318 100644 --- a/asr_worker.py +++ b/asr_worker.py @@ -1,6 +1,8 @@ import gc +import importlib.metadata import inspect import logging +import os import sys import traceback from typing import Any @@ -51,6 +53,43 @@ def _parse_device(device: str) -> tuple[str, int]: return device, 0 +def _sherpa_onnx_cuda_wheel_available() -> bool: + try: + version = importlib.metadata.version("sherpa-onnx") + except importlib.metadata.PackageNotFoundError: + return False + return "+cuda" in version.lower() + + +def _resolve_sherpa_onnx_provider(provider: str, parsed_device: str) -> str: + provider = str(provider or "auto").lower() + if provider == "auto": + return ( + "cuda" + if parsed_device == "cuda" and _sherpa_onnx_cuda_wheel_available() + else "cpu" + ) + if provider == "cuda" and not _sherpa_onnx_cuda_wheel_available(): + raise RuntimeError( + "sherpa-onnx CUDA provider selected, but the installed package is " + "not a CUDA wheel. Install the CUDA sherpa-onnx wheel or select CPU." + ) + if provider not in ("cpu", "cuda"): + raise ValueError(f"Unsupported sherpa-onnx provider: {provider}") + return provider + + +def _is_sherpa_onnx_cuda_load_error(exc: Exception) -> bool: + message = str(exc).lower() + return ( + "executionprovider_cuda" in message + or "onnxruntime_providers_cuda" in message + or "cublas" in message + or "cudnn" in message + or "failed to load shared library" in message + ) + + def _load_engine(config: dict): from model_manager import MODELS_DIR, apply_cache_env @@ -88,8 +127,6 @@ def _load_engine(config: dict): gpu_backend = "cuda" device_index = int(config.get("crispasr_device_index", device_index)) os_env_device = str(device_index) - import os - os.environ["CRISPASR_ARG_DEVICE"] = os_env_device if config.get("crispasr_unified_memory", True): os.environ["GGML_CUDA_ENABLE_UNIFIED_MEMORY"] = "1" @@ -102,6 +139,44 @@ def _load_engine(config: dict): punc_model=config.get("crispasr_punc_model", "auto"), unified_memory=config.get("crispasr_unified_memory", True), ) + elif engine_type == "sherpa-onnx": + requested_provider = str(config.get("sherpa_onnx_provider", "auto")).lower() + provider = _resolve_sherpa_onnx_provider( + requested_provider, parsed_device + ) + if provider == "cuda": + os.environ["CUDA_VISIBLE_DEVICES"] = str(device_index) + + from asr_sherpa_onnx import SherpaOnnxEngine + + kwargs = { + "model_path": config["sherpa_onnx_model_path"], + "model_info": config["sherpa_onnx_model_info"], + "num_threads": int(config.get("sherpa_onnx_num_threads", 2)), + "language": language, + "decoding_method": config.get("sherpa_onnx_decoding_method", "greedy_search"), + "left_padding_seconds": float( + config.get("sherpa_onnx_left_padding_seconds", 0.3) + ), + "tail_padding_seconds": float( + config.get("sherpa_onnx_tail_padding_seconds", 0.5) + ), + } + try: + engine = SherpaOnnxEngine(provider=provider, **kwargs) + except RuntimeError as exc: + if ( + requested_provider == "auto" + and provider == "cuda" + and _is_sherpa_onnx_cuda_load_error(exc) + ): + log.warning( + "sherpa-onnx CUDA provider failed to load; falling back to CPU. " + f"Reason: {exc}" + ) + engine = SherpaOnnxEngine(provider="cpu", **kwargs) + else: + raise else: from asr_engine import ASREngine diff --git a/build_release.ps1 b/build_release.ps1 index dc5665c..514e189 100644 --- a/build_release.ps1 +++ b/build_release.ps1 @@ -208,6 +208,11 @@ if (Test-Path (Join-Path $Root "repair_torch_metadata.ps1")) { Install-CrispAsrNativeRuntime -PythonExe $Py -UseCuda ($Index -notlike "*cpu*") +& $Uv pip install --python $Py "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" +if ($LASTEXITCODE -ne 0) { + Write-Host "sherpa-onnx install failed; sherpa-onnx ASR will be unavailable until installed manually" -ForegroundColor Yellow +} + & $Uv pip install --python $Py funasr --no-deps Write-Host "Setup complete." -ForegroundColor Green diff --git a/config.yaml b/config.yaml index dbd79a0..22dfde0 100644 --- a/config.yaml +++ b/config.yaml @@ -9,7 +9,7 @@ audio: chunk_duration: 0.032 asr: - # ASR engine: whisper, funasr, anime-whisper, crispasr + # ASR engine: whisper, funasr, anime-whisper, crispasr, sherpa-onnx asr_engine: "funasr" # Model size: tiny, base, small, medium, large-v3 model_size: "medium" @@ -22,6 +22,16 @@ asr: crispasr_device_index: 0 crispasr_punc_model: "auto" crispasr_unified_memory: true + # sherpa-onnx local model directory. Empty means user has not selected a model yet. + sherpa_onnx_model: "" + # sherpa-onnx provider: auto, cpu, cuda. Auto uses CUDA only with a CUDA sherpa-onnx wheel and CUDA ASR device. + sherpa_onnx_provider: "auto" + sherpa_onnx_num_threads: 2 + sherpa_onnx_decoding_method: "greedy_search" + # Left silence for sherpa-onnx OnlineRecognizer segment wrapper. + sherpa_onnx_left_padding_seconds: 0.3 + # Tail silence for sherpa-onnx OnlineRecognizer segment wrapper. + sherpa_onnx_tail_padding_seconds: 0.5 # Device: cuda or cpu device: "cuda" # Compute type: float16, int8_float16, int8 diff --git a/control_panel.py b/control_panel.py index 0bff422..d6c2c1e 100644 --- a/control_panel.py +++ b/control_panel.py @@ -43,9 +43,11 @@ get_cache_entries, list_local_crispasr_models, list_local_faster_whisper_models, + list_local_sherpa_onnx_models, migrate_funasr_settings, normalize_funasr_model_key, resolve_custom_crispasr_model, + resolve_custom_sherpa_onnx_model, resolve_custom_whisper_model, ) from i18n import t, LANGUAGES @@ -128,21 +130,17 @@ def _create_vad_tab(self): self._asr_engine = QComboBox() self._asr_engine.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) - self._asr_engine.addItems( - [ - f"[{t('asr_accurate')}] Whisper (faster-whisper)", - f"[{t('asr_fast')}] FunASR", - "Anime-Whisper (ja, anime/galgame)", - "CrispASR (ggml)", - ] - ) - engine_map_idx = { - "whisper": 0, - "funasr": 1, - "anime-whisper": 2, - "crispasr": 3, - } - engine_idx = engine_map_idx.get(s.get("asr_engine"), 0) + for label, key in ( + (f"[{t('asr_accurate')}] Whisper (faster-whisper)", "whisper"), + (f"[{t('asr_fast')}] FunASR", "funasr"), + ("Anime-Whisper (ja, anime/galgame)", "anime-whisper"), + ("CrispASR (ggml)", "crispasr"), + ("sherpa-onnx (ONNX)", "sherpa-onnx"), + ): + self._asr_engine.addItem(label, key) + engine_idx = self._asr_engine.findData(s.get("asr_engine")) + if engine_idx < 0: + engine_idx = self._asr_engine.findData("funasr") self._asr_engine.setCurrentIndex(engine_idx) asr_layout.addWidget(QLabel(t("label_engine")), 0, 0) asr_layout.addWidget(self._asr_engine, 0, 1) @@ -253,6 +251,45 @@ def _create_vad_tab(self): ) asr_layout.addWidget(self._crispasr_unified_memory, 7, 1) + self._sherpa_onnx_provider_label = QLabel(t("label_sherpa_onnx_provider")) + self._sherpa_onnx_provider = QComboBox() + for label, value in (("Auto", "auto"), ("CPU", "cpu"), ("CUDA", "cuda")): + self._sherpa_onnx_provider.addItem(label, value) + provider = str(s.get("sherpa_onnx_provider", "auto")).lower() + provider_idx = self._sherpa_onnx_provider.findData(provider) + if provider_idx >= 0: + self._sherpa_onnx_provider.setCurrentIndex(provider_idx) + self._sherpa_onnx_provider.currentIndexChanged.connect( + self._on_sherpa_onnx_setting_changed + ) + asr_layout.addWidget(self._sherpa_onnx_provider_label, 8, 0) + asr_layout.addWidget(self._sherpa_onnx_provider, 8, 1) + + self._sherpa_onnx_threads_label = QLabel(t("label_sherpa_onnx_threads")) + self._sherpa_onnx_num_threads = QSpinBox() + self._sherpa_onnx_num_threads.setRange(1, 32) + self._sherpa_onnx_num_threads.setValue( + int(s.get("sherpa_onnx_num_threads", 2) or 2) + ) + self._sherpa_onnx_num_threads.valueChanged.connect( + self._on_sherpa_onnx_setting_changed + ) + asr_layout.addWidget(self._sherpa_onnx_threads_label, 9, 0) + asr_layout.addWidget(self._sherpa_onnx_num_threads, 9, 1) + + self._sherpa_onnx_decoding_label = QLabel(t("label_sherpa_onnx_decoding")) + self._sherpa_onnx_decoding_method = QComboBox() + self._sherpa_onnx_decoding_method.addItem("greedy_search", "greedy_search") + decoding = str(s.get("sherpa_onnx_decoding_method", "greedy_search")) + decoding_idx = self._sherpa_onnx_decoding_method.findData(decoding) + if decoding_idx >= 0: + self._sherpa_onnx_decoding_method.setCurrentIndex(decoding_idx) + self._sherpa_onnx_decoding_method.currentIndexChanged.connect( + self._on_sherpa_onnx_setting_changed + ) + asr_layout.addWidget(self._sherpa_onnx_decoding_label, 10, 0) + asr_layout.addWidget(self._sherpa_onnx_decoding_method, 10, 1) + self._whisper_pad_label = QLabel(t("label_whisper_padding")) self._whisper_pad_seconds = QDoubleSpinBox() self._whisper_pad_seconds.setRange(0.0, 5.0) @@ -266,8 +303,8 @@ def _create_vad_tab(self): self._whisper_pad_seconds.setSuffix(" s") self._whisper_pad_seconds.setSpecialValueText(t("whisper_padding_off")) self._whisper_pad_seconds.setToolTip(t("whisper_padding_tooltip")) - asr_layout.addWidget(self._whisper_pad_label, 8, 0) - asr_layout.addWidget(self._whisper_pad_seconds, 8, 1) + asr_layout.addWidget(self._whisper_pad_label, 11, 0) + asr_layout.addWidget(self._whisper_pad_seconds, 11, 1) self._whisper_pad_seconds.valueChanged.connect(self._auto_save) self._sensevoice_pad_label = QLabel(t("label_sensevoice_padding")) @@ -283,8 +320,8 @@ def _create_vad_tab(self): self._sensevoice_pad_seconds.setSuffix(" s") self._sensevoice_pad_seconds.setSpecialValueText(t("sensevoice_padding_off")) self._sensevoice_pad_seconds.setToolTip(t("sensevoice_padding_tooltip")) - asr_layout.addWidget(self._sensevoice_pad_label, 9, 0) - asr_layout.addWidget(self._sensevoice_pad_seconds, 9, 1) + asr_layout.addWidget(self._sensevoice_pad_label, 12, 0) + asr_layout.addWidget(self._sensevoice_pad_seconds, 12, 1) self._sensevoice_pad_seconds.valueChanged.connect(self._auto_save) self._audio_device = QComboBox() @@ -306,8 +343,8 @@ def _create_vad_tab(self): self._audio_device.setCurrentIndex(idx) else: self._audio_device.setCurrentIndex(1) # system default - asr_layout.addWidget(QLabel(t("label_audio")), 10, 0) - asr_layout.addWidget(self._audio_device, 10, 1) + asr_layout.addWidget(QLabel(t("label_audio")), 13, 0) + asr_layout.addWidget(self._audio_device, 13, 1) self._audio_device.currentIndexChanged.connect(self._auto_save) self._mic_device = QComboBox() @@ -328,16 +365,16 @@ def _create_vad_tab(self): idx = self._mic_device.findText(saved_mic) if idx >= 0: self._mic_device.setCurrentIndex(idx) - asr_layout.addWidget(QLabel(t("label_mic")), 11, 0) - asr_layout.addWidget(self._mic_device, 11, 1) + asr_layout.addWidget(QLabel(t("label_mic")), 14, 0) + asr_layout.addWidget(self._mic_device, 14, 1) self._mic_device.currentIndexChanged.connect(self._auto_save) self._hub_combo = QComboBox() self._hub_combo.addItems([t("hub_modelscope"), t("hub_huggingface")]) saved_hub = s.get("hub", "ms") self._hub_combo.setCurrentIndex(0 if saved_hub == "ms" else 1) - asr_layout.addWidget(QLabel(t("label_hub")), 12, 0) - asr_layout.addWidget(self._hub_combo, 12, 1) + asr_layout.addWidget(QLabel(t("label_hub")), 15, 0) + asr_layout.addWidget(self._hub_combo, 15, 1) self._hub_combo.currentIndexChanged.connect(self._auto_save) self._ui_lang_combo = QComboBox() @@ -346,8 +383,8 @@ def _create_vad_tab(self): saved_lang = s.get("ui_lang", get_lang()) self._ui_lang_combo.setCurrentIndex(0 if saved_lang == "en" else 1) - asr_layout.addWidget(QLabel(t("label_ui_lang")), 13, 0) - asr_layout.addWidget(self._ui_lang_combo, 13, 1) + asr_layout.addWidget(QLabel(t("label_ui_lang")), 16, 0) + asr_layout.addWidget(self._ui_lang_combo, 16, 1) self._ui_lang_combo.currentIndexChanged.connect(self._on_ui_lang_changed) layout.addWidget(asr_group) @@ -371,7 +408,7 @@ def _create_vad_tab(self): self._whisper_dl_btn.clicked.connect(self._download_whisper) whisper_layout.addWidget(self._whisper_dl_btn) layout.addWidget(self._whisper_group) - self._whisper_group.setVisible(engine_idx == 0) + self._whisper_group.setVisible(self._selected_asr_engine() == "whisper") self._crispasr_group = QGroupBox(t("group_crispasr_model")) crispasr_layout = QHBoxLayout(self._crispasr_group) @@ -386,13 +423,36 @@ def _create_vad_tab(self): self._crispasr_status.setStyleSheet("color: #888; font-size: 11px;") crispasr_layout.addWidget(self._crispasr_status, 1) layout.addWidget(self._crispasr_group) - self._crispasr_group.setVisible(engine_idx == 3) + self._crispasr_group.setVisible(self._selected_asr_engine() == "crispasr") + + self._sherpa_onnx_group = QGroupBox(t("group_sherpa_onnx_models")) + sherpa_layout = QHBoxLayout(self._sherpa_onnx_group) + self._sherpa_onnx_model_combo = QComboBox() + saved_sherpa_model = s.get("sherpa_onnx_model", "") + self._populate_sherpa_onnx_models(saved_sherpa_model) + self._sherpa_onnx_model_combo.currentIndexChanged.connect( + self._on_sherpa_onnx_model_changed + ) + sherpa_layout.addWidget(self._sherpa_onnx_model_combo) + self._sherpa_onnx_status = QLabel("") + self._sherpa_onnx_status.setStyleSheet("color: #888; font-size: 11px;") + sherpa_layout.addWidget(self._sherpa_onnx_status, 1) + self._sherpa_onnx_refresh_btn = QPushButton(t("btn_refresh_sherpa_onnx_models")) + self._sherpa_onnx_refresh_btn.clicked.connect( + self._refresh_sherpa_onnx_models + ) + sherpa_layout.addWidget(self._sherpa_onnx_refresh_btn) + layout.addWidget(self._sherpa_onnx_group) + self._sherpa_onnx_group.setVisible( + self._selected_asr_engine() == "sherpa-onnx" + ) self._asr_engine.currentIndexChanged.connect( self._on_asr_engine_changed ) self._on_asr_engine_changed(engine_idx) self._update_whisper_size_label() self._update_crispasr_status() + self._update_sherpa_onnx_status() mode_group = QGroupBox(t("group_vad_mode")) mode_layout = QVBoxLayout(mode_group) @@ -1090,12 +1150,21 @@ def _get_asr_lang_code(self) -> str: """Get the language code from the ASR language combo (stored as userData).""" return self._asr_lang.currentData() or "auto" + def _selected_asr_engine(self) -> str: + value = self._asr_engine.currentData() + return str(value) if value else "funasr" + def _on_asr_engine_changed(self, index): - self._whisper_group.setVisible(index == 0) - is_funasr = index == 1 - is_crispasr = index == 3 + engine = self._selected_asr_engine() + is_whisper = engine == "whisper" + is_funasr = engine == "funasr" + is_crispasr = engine == "crispasr" + is_sherpa_onnx = engine == "sherpa-onnx" + self._whisper_group.setVisible(is_whisper) if hasattr(self, "_crispasr_group"): self._crispasr_group.setVisible(is_crispasr) + if hasattr(self, "_sherpa_onnx_group"): + self._sherpa_onnx_group.setVisible(is_sherpa_onnx) if hasattr(self, "_funasr_model_combo"): self._funasr_model_label.setVisible(is_funasr) self._funasr_model_combo.setVisible(is_funasr) @@ -1107,8 +1176,14 @@ def _on_asr_engine_changed(self, index): self._crispasr_punc_label.setVisible(is_crispasr) self._crispasr_punc_model.setVisible(is_crispasr) self._crispasr_unified_memory.setVisible(is_crispasr) + if hasattr(self, "_sherpa_onnx_provider"): + self._sherpa_onnx_provider_label.setVisible(is_sherpa_onnx) + self._sherpa_onnx_provider.setVisible(is_sherpa_onnx) + self._sherpa_onnx_threads_label.setVisible(is_sherpa_onnx) + self._sherpa_onnx_num_threads.setVisible(is_sherpa_onnx) + self._sherpa_onnx_decoding_label.setVisible(is_sherpa_onnx) + self._sherpa_onnx_decoding_method.setVisible(is_sherpa_onnx) if hasattr(self, "_whisper_pad_seconds"): - is_whisper = index == 0 self._whisper_pad_label.setVisible(is_whisper) self._whisper_pad_seconds.setVisible(is_whisper) if hasattr(self, "_sensevoice_pad_seconds"): @@ -1164,10 +1239,98 @@ def _on_crispasr_runtime_setting_changed(self): ) self._auto_save() + def _selected_sherpa_onnx_model(self) -> str: + value = self._sherpa_onnx_model_combo.currentData() + return str(value) if value is not None else "" + + def _selected_sherpa_onnx_provider(self) -> str: + value = self._sherpa_onnx_provider.currentData() + return str(value) if value else "auto" + + def _selected_sherpa_onnx_decoding_method(self) -> str: + value = self._sherpa_onnx_decoding_method.currentData() + return str(value) if value else "greedy_search" + + def _on_sherpa_onnx_model_changed(self): + self._current_settings["sherpa_onnx_model"] = ( + self._selected_sherpa_onnx_model() + ) + self._update_sherpa_onnx_status() + self._auto_save() + + def _on_sherpa_onnx_setting_changed(self): + self._current_settings["sherpa_onnx_provider"] = ( + self._selected_sherpa_onnx_provider() + ) + self._current_settings["sherpa_onnx_num_threads"] = ( + self._sherpa_onnx_num_threads.value() + ) + self._current_settings["sherpa_onnx_decoding_method"] = ( + self._selected_sherpa_onnx_decoding_method() + ) + self._auto_save() + def _selected_whisper_model(self) -> str: value = self._whisper_size_combo.currentData() return str(value) if value else self._whisper_size_combo.currentText() + def _populate_sherpa_onnx_models(self, saved_value: str): + self._sherpa_onnx_model_combo.clear() + self._sherpa_onnx_model_combo.addItem(t("sherpa_onnx_model_placeholder"), "") + + local_prefix = t("sherpa_onnx_local_prefix") + for item in list_local_sherpa_onnx_models(): + idx = self._sherpa_onnx_model_combo.count() + family = str(item.get("family") or "").replace("_", " ") + suffix = f" [{family}]" if family else "" + self._sherpa_onnx_model_combo.addItem( + f"{local_prefix}: {item['name']}{suffix}", item["path"] + ) + self._sherpa_onnx_model_combo.setItemData( + idx, item["path"], Qt.ItemDataRole.ToolTipRole + ) + + if not saved_value: + selected = "" + else: + selected = resolve_custom_sherpa_onnx_model(saved_value) or saved_value + idx = self._sherpa_onnx_model_combo.findData(selected) + if idx < 0: + idx = self._sherpa_onnx_model_combo.findText(saved_value) + if idx < 0 and selected: + label = f"{t('sherpa_onnx_missing_local')}: {Path(str(selected)).name}" + idx = self._sherpa_onnx_model_combo.count() + self._sherpa_onnx_model_combo.addItem(label, selected) + self._sherpa_onnx_model_combo.setItemData( + idx, str(selected), Qt.ItemDataRole.ToolTipRole + ) + if idx >= 0: + self._sherpa_onnx_model_combo.setCurrentIndex(idx) + + def _refresh_sherpa_onnx_models(self): + saved = self._selected_sherpa_onnx_model() + self._populate_sherpa_onnx_models(saved) + self._update_sherpa_onnx_status() + + def _update_sherpa_onnx_status(self): + from model_manager import is_asr_cached + + model_key = self._selected_sherpa_onnx_model() + if not model_key: + if list_local_sherpa_onnx_models(): + self._sherpa_onnx_status.setText(t("sherpa_onnx_select_model")) + else: + self._sherpa_onnx_status.setText(t("sherpa_onnx_no_local_models")) + self._sherpa_onnx_status.setStyleSheet("color: #888; font-size: 11px;") + return + cached = is_asr_cached("sherpa-onnx", model_key, self._current_settings.get("hub", "ms")) + if cached: + self._sherpa_onnx_status.setText(t("sherpa_onnx_local_ready")) + self._sherpa_onnx_status.setStyleSheet("color: #4a4; font-size: 11px;") + else: + self._sherpa_onnx_status.setText(t("sherpa_onnx_invalid_local")) + self._sherpa_onnx_status.setStyleSheet("color: #d66; font-size: 11px;") + def _populate_crispasr_models(self, saved_value: str): self._crispasr_model_combo.clear() self._crispasr_model_combo.addItem(t("crispasr_model_placeholder"), "") @@ -1561,15 +1724,7 @@ def _apply_prompt(self): def _apply_settings(self): self._current_settings["asr_language"] = self._get_asr_lang_code() - engine_map = { - 0: "whisper", - 1: "funasr", - 2: "anime-whisper", - 3: "crispasr", - } - self._current_settings["asr_engine"] = engine_map.get( - self._asr_engine.currentIndex(), "whisper" - ) + self._current_settings["asr_engine"] = self._selected_asr_engine() self._current_settings["funasr_model"] = self._selected_funasr_model() self._current_settings["whisper_model_size"] = ( self._selected_whisper_model() @@ -1588,6 +1743,18 @@ def _apply_settings(self): self._current_settings["crispasr_unified_memory"] = ( self._crispasr_unified_memory.isChecked() ) + self._current_settings["sherpa_onnx_model"] = ( + self._selected_sherpa_onnx_model() + ) + self._current_settings["sherpa_onnx_provider"] = ( + self._selected_sherpa_onnx_provider() + ) + self._current_settings["sherpa_onnx_num_threads"] = ( + self._sherpa_onnx_num_threads.value() + ) + self._current_settings["sherpa_onnx_decoding_method"] = ( + self._selected_sherpa_onnx_decoding_method() + ) dev_text = self._asr_device.currentText() self._current_settings["asr_device"] = dev_text.split(" (")[0] audio_idx = self._audio_device.currentIndex() diff --git a/i18n/CHANGELOG_en.md b/i18n/CHANGELOG_en.md index a78e798..654cb27 100644 --- a/i18n/CHANGELOG_en.md +++ b/i18n/CHANGELOG_en.md @@ -1,5 +1,12 @@ # Changelog +## 2026-06-20 +- New ASR engine: sherpa-onnx (ONNX OfflineRecognizer), reusing the existing VAD segmentation, ASR worker subprocess, and translation pipeline +- Settings panel can select a local sherpa-onnx model directory, provider, and thread count; local scanning supports SenseVoice / Paraformer / Moonshine / Whisper directory layouts, plus online transducer snapshots with `encoder.onnx` / `decoder.onnx` / `joiner.onnx` / `tokens.txt` +- sherpa-onnx online transducer models use `OnlineRecognizer` as a VAD segment wrapper in this phase; partial streaming ASR is not enabled yet +- sherpa-onnx does not use a built-in downloader in this phase: extract official models anywhere under `models/`, then refresh and select them in settings +- Installer now installs the CPU sherpa-onnx runtime by default, with `-SherpaOnnxRuntime cuda11/cuda12` for CUDA wheels + ## 2026-06-19 - Fix CrispASR startup failure with `No module named 'crispasr'`: added the CrispASR Python binding to `pyproject.toml`, pinned to GitHub Releases `v0.7.2` - Installer now downloads the prebuilt Windows `libcrispasr` DLL runtime from Releases and places it next to the installed `crispasr` package diff --git a/i18n/CHANGELOG_zh.md b/i18n/CHANGELOG_zh.md index 4263395..46b6bf2 100644 --- a/i18n/CHANGELOG_zh.md +++ b/i18n/CHANGELOG_zh.md @@ -1,5 +1,12 @@ # 更新日志 +## 2026-06-20 +- 新增 ASR 引擎: sherpa-onnx (ONNX OfflineRecognizer), 复用现有 VAD 切段、ASR worker 子进程和翻译管线 +- 设置面板可选择 sherpa-onnx 本地模型目录、provider 和线程数; 模型扫描支持 SenseVoice / Paraformer / Moonshine / Whisper 目录结构,以及包含 `encoder.onnx` / `decoder.onnx` / `joiner.onnx` / `tokens.txt` 的 online transducer snapshot +- sherpa-onnx online transducer 模型当前通过 `OnlineRecognizer` 对 VAD 切段做整段识别; 暂未启用 partial streaming ASR +- sherpa-onnx 默认不内置下载器: 将官方模型解压到 `models/` 下任意子目录后在设置中刷新选择 +- 安装脚本默认安装 CPU 版 sherpa-onnx runtime, 可通过 `-SherpaOnnxRuntime cuda11/cuda12` 安装 CUDA wheel + ## 2026-06-19 - 修复 CrispASR 启动时报 `No module named 'crispasr'`: `pyproject.toml` 新增 CrispASR Python binding 依赖, 固定到 GitHub Releases `v0.7.2` - 安装脚本新增 CrispASR 原生运行时安装: 从 Releases 下载预编译 `libcrispasr` Windows DLL, 放入已安装的 `crispasr` 包目录 diff --git a/i18n/en.yaml b/i18n/en.yaml index f77781e..bf5299d 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -86,6 +86,10 @@ label_crispasr_gpu_backend: "CrispASR GPU:" label_crispasr_device_index: "CrispASR device:" label_crispasr_punc_model: "CrispASR punctuation:" label_crispasr_unified_memory: "Enable unified memory" +label_sherpa_onnx_model: "sherpa-onnx model:" +label_sherpa_onnx_provider: "sherpa-onnx provider:" +label_sherpa_onnx_threads: "sherpa-onnx threads:" +label_sherpa_onnx_decoding: "sherpa-onnx decoding:" label_whisper_padding: "Whisper padding:" whisper_padding_off: "Off" whisper_padding_tooltip: "Pads faster-whisper input to this duration bucket. 0 disables padding." @@ -164,6 +168,15 @@ crispasr_local_prefix: "Local" crispasr_local_ready: "Local model" crispasr_missing_local: "Missing" crispasr_invalid_local: "Local model unavailable" +group_sherpa_onnx_models: "sherpa-onnx Models" +btn_refresh_sherpa_onnx_models: "Refresh" +sherpa_onnx_model_placeholder: "Select sherpa-onnx model" +sherpa_onnx_select_model: "Select a model" +sherpa_onnx_no_local_models: "No local models found" +sherpa_onnx_local_prefix: "Local" +sherpa_onnx_local_ready: "Local model" +sherpa_onnx_missing_local: "Missing" +sherpa_onnx_invalid_local: "Local model unavailable" btn_open_folder: "Open Models Folder" btn_delete_all_exit: "Delete All && Exit" scanning: "Scanning..." diff --git a/i18n/zh.yaml b/i18n/zh.yaml index 093eb39..0ab9231 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -86,6 +86,10 @@ label_crispasr_gpu_backend: "CrispASR GPU:" label_crispasr_device_index: "CrispASR 设备:" label_crispasr_punc_model: "CrispASR 标点:" label_crispasr_unified_memory: "启用统一内存" +label_sherpa_onnx_model: "sherpa-onnx 模型:" +label_sherpa_onnx_provider: "sherpa-onnx Provider:" +label_sherpa_onnx_threads: "sherpa-onnx 线程:" +label_sherpa_onnx_decoding: "sherpa-onnx 解码:" label_whisper_padding: "Whisper 补零:" whisper_padding_off: "关闭" whisper_padding_tooltip: "将 faster-whisper 输入补齐到指定秒数档位。0 表示关闭。" @@ -164,6 +168,15 @@ crispasr_local_prefix: "本地" crispasr_local_ready: "本地模型" crispasr_missing_local: "缺失" crispasr_invalid_local: "本地模型不可用" +group_sherpa_onnx_models: "sherpa-onnx 模型" +btn_refresh_sherpa_onnx_models: "刷新" +sherpa_onnx_model_placeholder: "请选择 sherpa-onnx 模型" +sherpa_onnx_select_model: "请选择模型" +sherpa_onnx_no_local_models: "未扫描到本地模型" +sherpa_onnx_local_prefix: "本地" +sherpa_onnx_local_ready: "本地模型" +sherpa_onnx_missing_local: "缺失" +sherpa_onnx_invalid_local: "本地模型不可用" btn_open_folder: "打开模型文件夹" btn_delete_all_exit: "删除全部并退出" scanning: "扫描中..." diff --git a/install.ps1 b/install.ps1 index c9919ae..ec24b70 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,6 +1,11 @@ # LiveTranslate - One-click installer # Usage: Double-click install.bat (or run: powershell -ExecutionPolicy Bypass -File install.ps1) +param( + [ValidateSet("cpu", "cuda11", "cuda12")] + [string]$SherpaOnnxRuntime = "cpu" +) + $ErrorActionPreference = "Stop" $ProjectDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ProjectDir @@ -67,6 +72,32 @@ function Install-CrispAsrNativeRuntime { } } +function Install-SherpaOnnxRuntime { + param( + [string]$PythonExe, + [string]$Runtime + ) + + Write-Step "Installing sherpa-onnx runtime ($Runtime)..." + try { + if ($Runtime -eq "cpu") { + & $Uv pip install --python $PythonExe "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" + } else { + & $Uv pip uninstall --python $PythonExe sherpa-onnx sherpa-onnx-bin sherpa-onnx-core + if ($Runtime -eq "cuda11") { + & $Uv pip install --python $PythonExe --verbose sherpa-onnx=="1.13.3+cuda" --no-index -f https://k2-fsa.github.io/sherpa/onnx/cuda.html + } elseif ($Runtime -eq "cuda12") { + & $Uv pip install --python $PythonExe --verbose sherpa-onnx=="1.13.3+cuda12.cudnn9" -f https://k2-fsa.github.io/sherpa/onnx/cuda.html + } + } + if ($LASTEXITCODE -ne 0) { throw "sherpa-onnx install command failed" } + Write-Ok "sherpa-onnx runtime installed ($Runtime)" + } catch { + Write-Warn "sherpa-onnx runtime installation failed: $($_.Exception.Message)" + Write-Warn "sherpa-onnx ASR will not run until the Python package is installed" + } +} + function Enable-SystemProxy { # uv (Python download) and pip honor *_PROXY env vars but not the Windows # registry system proxy; bridge it here. An already-set env proxy wins. @@ -381,7 +412,10 @@ if (Test-Path ".\repair_torch_metadata.ps1") { # ── Step 6: Install CrispASR native runtime ── Install-CrispAsrNativeRuntime -PythonExe $Python -UseCuda $HasNvidia -# ── Step 7: Install FunASR (no-deps) ── +# ── Step 7: Install sherpa-onnx runtime ── +Install-SherpaOnnxRuntime -PythonExe $Python -Runtime $SherpaOnnxRuntime + +# ── Step 8: Install FunASR (no-deps) ── Write-Step "Installing FunASR (--no-deps)..." & $Uv pip install --python $Python funasr --no-deps diff --git a/main.py b/main.py index 9e2f8dd..9d382dc 100644 --- a/main.py +++ b/main.py @@ -254,6 +254,10 @@ def _on_settings_changed(self, settings): "crispasr_device_index", "crispasr_punc_model", "crispasr_unified_memory", + "sherpa_onnx_model", + "sherpa_onnx_provider", + "sherpa_onnx_num_threads", + "sherpa_onnx_decoding_method", "hub", ) ): @@ -325,6 +329,13 @@ def _switch_asr_engine(self, engine_type: str): settings = self._panel.get_settings() if self._panel else {} plan = self._asr_service.prepare_switch(engine_type, settings) if plan.already_current: + if plan.error: + parent = ( + self._panel + if self._panel and self._panel.isVisible() + else self._overlay + ) + QMessageBox.warning(parent, t("error_title"), plan.error) return # Reset interim state for the engine boundary. The active worker is @@ -717,6 +728,10 @@ def main(): config.setdefault("asr", {}) config["asr"].setdefault("asr_engine", "funasr") config["asr"].setdefault("funasr_model", DEFAULT_FUNASR_MODEL) + config["asr"].setdefault("sherpa_onnx_model", "") + config["asr"].setdefault("sherpa_onnx_provider", "auto") + config["asr"].setdefault("sherpa_onnx_num_threads", 2) + config["asr"].setdefault("sherpa_onnx_decoding_method", "greedy_search") saved = load_settings(config) migrate_funasr_settings(saved) @@ -804,6 +819,11 @@ def main(): "crispasr_model", config["asr"].get("crispasr_model", ""), ) + elif current_engine == "sherpa-onnx": + startup_model_key = saved.get( + "sherpa_onnx_model", + config["asr"].get("sherpa_onnx_model", ""), + ) else: startup_model_key = saved.get("whisper_model_size", config["asr"]["model_size"]) missing = get_missing_models( diff --git a/model_manager.py b/model_manager.py index 2a644e8..2dae675 100644 --- a/model_manager.py +++ b/model_manager.py @@ -1,5 +1,6 @@ import os import contextlib +import json import logging from pathlib import Path @@ -106,6 +107,7 @@ def _proxy_env(proxy: str): } DEFAULT_FUNASR_MODEL = "sensevoice-small" +DEFAULT_SHERPA_ONNX_MODEL = "" FUNASR_LEGACY_ENGINE_ALIASES = { "sensevoice": "sensevoice-small", @@ -140,6 +142,7 @@ def asr_model_id( "whisper": "Whisper", "anime-whisper": "Anime-Whisper", "crispasr": "CrispASR", + "sherpa-onnx": "sherpa-onnx", } _CRISPASR_EXTS = {".gguf", ".bin"} @@ -406,6 +409,285 @@ def local_crispasr_display_name(path) -> str | None: return Path(resolved).name +def _custom_sherpa_onnx_path(value) -> Path | None: + if not value: + return None + path = Path(str(value)).expanduser() + if not path.is_absolute(): + path = APP_DIR / path + return path + + +def _read_sherpa_onnx_metadata(path: Path) -> dict | None: + metadata_path = path / "sherpa_onnx_model.json" + if not metadata_path.is_file(): + return None + try: + data = json.loads(metadata_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + log.warning(f"Invalid sherpa-onnx metadata: {metadata_path}: {exc}") + return {"_invalid": True} + return data if isinstance(data, dict) else {"_invalid": True} + + +def _sherpa_file(path: Path, metadata: dict, *keys: str, default: str | None = None): + for key in keys: + value = metadata.get(key) + if value: + candidate = Path(str(value)) + if not candidate.is_absolute(): + candidate = path / candidate + if candidate.is_file(): + return str(candidate.resolve()) + if default: + candidate = path / default + if candidate.is_file(): + return str(candidate.resolve()) + return None + + +def _first_glob_file(path: Path, *patterns: str) -> str | None: + for pattern in patterns: + matches = sorted(path.glob(pattern)) + for match in matches: + if match.is_file(): + return str(match.resolve()) + return None + + +def _sherpa_family_hint(path: Path, metadata: dict) -> str | None: + family = str(metadata.get("family") or "").strip().lower().replace("-", "_") + if family: + aliases = { + "sensevoice": "sense_voice", + "sense_voice": "sense_voice", + "paraformer": "paraformer", + "moonshine": "moonshine", + "whisper": "whisper", + "online_transducer": "online_transducer", + "online-transducer": "online_transducer", + } + return aliases.get(family) + + name = path.name.lower().replace("-", "_") + if "sense_voice" in name or "sensevoice" in name: + return "sense_voice" + if "paraformer" in name: + return "paraformer" + if "moonshine" in name: + return "moonshine" + if name.startswith("sherpa_onnx_whisper") or name.startswith("sherpa-onnx-whisper"): + return "whisper" + return None + + +def _sherpa_display_name(path: Path, metadata: dict) -> str: + display_name = metadata.get("display_name") or metadata.get("name") + if display_name: + return str(display_name).strip() + return _hf_snapshot_name(path) or path.name + + +def _is_sherpa_online_transducer_dir(path: Path, metadata: dict, family: str | None) -> bool: + encoder = _sherpa_file( + path, metadata, "encoder", "encoder_file", default="encoder.onnx" + ) or _first_glob_file(path, "encoder.int8.onnx", "encoder*.onnx") + decoder = _sherpa_file( + path, metadata, "decoder", "decoder_file", default="decoder.onnx" + ) or _first_glob_file(path, "decoder.int8.onnx", "decoder*.onnx") + joiner = _sherpa_file( + path, metadata, "joiner", "joiner_file", default="joiner.onnx" + ) or _first_glob_file(path, "joiner.int8.onnx", "joiner*.onnx") + tokens = _sherpa_file(path, metadata, "tokens", "tokens_file", default="tokens.txt") + if encoder and decoder and joiner and tokens: + return True + + if family == "online_transducer": + joint = _sherpa_file(path, metadata, "joint", "joint_file", default="joint.onnx") + return bool(encoder and decoder and joint and tokens) + return False + + +def detect_sherpa_onnx_model_dir(path) -> dict | None: + """Return normalized sherpa-onnx model metadata when a directory is usable.""" + if not path: + return None + path = Path(path) + if not path.is_dir(): + return None + try: + path = path.resolve() + except OSError: + return None + + metadata = _read_sherpa_onnx_metadata(path) or {} + if metadata.get("_invalid"): + return None + + family = _sherpa_family_hint(path, metadata) + if family is None and _is_sherpa_online_transducer_dir(path, metadata, family): + family = "online_transducer" + tokens_file = _sherpa_file(path, metadata, "tokens", "tokens_file", default="tokens.txt") + sample_rate = int(metadata.get("sample_rate") or 16000) + feature_dim = int(metadata.get("feature_dim") or 80) + + base = { + "path": str(path), + "family": family, + "display_name": _sherpa_display_name(path, metadata), + "sample_rate": sample_rate, + "feature_dim": feature_dim, + } + + if family == "sense_voice": + model_file = ( + _sherpa_file(path, metadata, "model", "model_file") + or _first_glob_file(path, "model.int8.onnx", "model.onnx") + ) + if tokens_file and model_file: + return {**base, "tokens_file": tokens_file, "model_file": model_file} + return None + + if family == "paraformer": + model_file = ( + _sherpa_file(path, metadata, "model", "model_file", "paraformer") + or _first_glob_file(path, "model.onnx") + ) + if tokens_file and model_file: + return {**base, "tokens_file": tokens_file, "model_file": model_file} + return None + + if family == "moonshine": + preprocessor = ( + _sherpa_file(path, metadata, "preprocessor", "preprocessor_file", "preprocess") + or _first_glob_file(path, "preprocess.onnx", "preprocessor.onnx") + ) + encoder = _sherpa_file(path, metadata, "encoder", "encoder_file") or _first_glob_file( + path, "encode*.onnx", "encoder*.onnx" + ) + uncached_decoder = _sherpa_file( + path, metadata, "uncached_decoder", "uncached_decoder_file" + ) or _first_glob_file(path, "uncached_decode*.onnx", "uncached_decoder*.onnx") + cached_decoder = _sherpa_file( + path, metadata, "cached_decoder", "cached_decoder_file" + ) or _first_glob_file(path, "cached_decode*.onnx", "cached_decoder*.onnx") + if tokens_file and preprocessor and encoder and uncached_decoder and cached_decoder: + return { + **base, + "tokens_file": tokens_file, + "preprocessor_file": preprocessor, + "encoder_file": encoder, + "uncached_decoder_file": uncached_decoder, + "cached_decoder_file": cached_decoder, + } + return None + + if family == "whisper": + encoder = _sherpa_file(path, metadata, "encoder", "encoder_file") or _first_glob_file( + path, "*encoder*.onnx" + ) + decoder = _sherpa_file(path, metadata, "decoder", "decoder_file") or _first_glob_file( + path, "*decoder*.onnx" + ) + tokens = tokens_file or _first_glob_file(path, "*tokens.txt") + if encoder and decoder and tokens: + return {**base, "tokens_file": tokens, "encoder_file": encoder, "decoder_file": decoder} + return None + + if family == "online_transducer": + encoder = _sherpa_file( + path, metadata, "encoder", "encoder_file", default="encoder.onnx" + ) or _first_glob_file(path, "encoder.int8.onnx", "encoder*.onnx") + decoder = _sherpa_file( + path, metadata, "decoder", "decoder_file", default="decoder.onnx" + ) or _first_glob_file(path, "decoder.int8.onnx", "decoder*.onnx") + joiner = _sherpa_file( + path, metadata, "joiner", "joiner_file", default="joiner.onnx" + ) or _first_glob_file(path, "joiner.int8.onnx", "joiner*.onnx") + if not joiner: + joiner = _sherpa_file(path, metadata, "joint", "joint_file", default="joint.onnx") + if encoder and decoder and joiner and tokens_file: + info = { + **base, + "tokens_file": tokens_file, + "encoder_file": encoder, + "decoder_file": decoder, + "joiner_file": joiner, + } + for key in ("model_type", "modeling_unit", "bpe_vocab"): + value = metadata.get(key) + if value: + info[key] = str(value) + return info + return None + + return None + + +def is_sherpa_onnx_model_dir(path) -> bool: + return detect_sherpa_onnx_model_dir(path) is not None + + +def resolve_custom_sherpa_onnx_model(value) -> str | None: + path = _custom_sherpa_onnx_path(value) + if path and is_sherpa_onnx_model_dir(path): + return str(path.resolve()) + return None + + +def list_local_sherpa_onnx_models() -> list[dict]: + """Scan ./models recursively for recognizable local sherpa-onnx models.""" + if not MODELS_DIR.exists(): + return [] + + entries = [] + name_counts = {} + seen = set() + try: + dirs = [MODELS_DIR, *[p for p in MODELS_DIR.rglob("*") if p.is_dir()]] + except (OSError, PermissionError): + return [] + + for model_dir in dirs: + info = detect_sherpa_onnx_model_dir(model_dir) + if not info: + continue + identity = info["path"] + if identity in seen: + continue + seen.add(identity) + name = info["display_name"] + name_counts[name] = name_counts.get(name, 0) + 1 + if name_counts[name] > 1: + name = f"{name} ({model_dir.parent.name})" + entries.append( + { + "name": name, + "path": identity, + "family": info["family"], + "info": info, + } + ) + + entries.sort(key=lambda item: item["name"].lower()) + return entries + + +def local_sherpa_onnx_display_name(path) -> str | None: + resolved = resolve_custom_sherpa_onnx_model(path) + if not resolved: + return None + for item in list_local_sherpa_onnx_models(): + if item["path"] == resolved: + return item["name"] + info = detect_sherpa_onnx_model_dir(resolved) + return info["display_name"] if info else Path(resolved).name + + +def get_sherpa_onnx_model_path(value) -> str | None: + return resolve_custom_sherpa_onnx_model(value) + + def apply_cache_env(): """Point all model caches to ./models/.""" resolved = str(MODELS_DIR.resolve()) @@ -473,6 +755,8 @@ def _hf_repo_complete(org: str, name: str, min_bytes: int = 50_000_000) -> bool: def is_asr_cached(engine_type, model_size="medium", hub="ms") -> bool: if engine_type == "crispasr": return resolve_custom_crispasr_model(model_size) is not None + if engine_type == "sherpa-onnx": + return get_sherpa_onnx_model_path(model_size) is not None if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -534,7 +818,7 @@ def get_missing_models(engine, model_size, hub) -> list: if not is_asr_cached(engine, model_size, hub): if engine == "whisper" and model_size not in _WHISPER_SIZES: return missing - if engine == "crispasr": + if engine in ("crispasr", "sherpa-onnx"): return missing if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -564,7 +848,12 @@ def get_missing_models(engine, model_size, hub) -> list: return missing -def get_local_model_path(engine_type, hub="ms", funasr_model: str | None = None): +def get_local_model_path( + engine_type, + hub="ms", + funasr_model: str | None = None, + model_path_or_id: str | None = None, +): """Return local snapshot path if model is cached, else None. Checks the preferred hub first, then falls back to the other hub. @@ -572,6 +861,9 @@ def get_local_model_path(engine_type, hub="ms", funasr_model: str | None = None) if engine_type == "crispasr": model_value = funasr_model return resolve_custom_crispasr_model(model_value) + if engine_type == "sherpa-onnx": + model_value = model_path_or_id if model_path_or_id is not None else funasr_model + return get_sherpa_onnx_model_path(model_value) if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -764,6 +1056,9 @@ def get_cache_entries(): for item in list_local_crispasr_models(): entries.append((f"CrispASR Local: {item['name']}", Path(item["path"]))) + for item in list_local_sherpa_onnx_models(): + entries.append((f"sherpa-onnx Local: {item['name']}", Path(item["path"]))) + if torch_base.exists(): for d in sorted(torch_base.glob("snakers4_silero-vad*")): if d.is_dir(): diff --git a/settings_store.py b/settings_store.py index 5623f96..37edf61 100644 --- a/settings_store.py +++ b/settings_store.py @@ -49,6 +49,25 @@ def normalize_settings(config: dict[str, Any], saved: dict | None = None) -> dic settings.setdefault( "crispasr_unified_memory", asr.get("crispasr_unified_memory", True) ) + settings.setdefault("sherpa_onnx_model", asr.get("sherpa_onnx_model", "")) + settings.setdefault( + "sherpa_onnx_provider", asr.get("sherpa_onnx_provider", "auto") + ) + settings.setdefault( + "sherpa_onnx_num_threads", asr.get("sherpa_onnx_num_threads", 2) + ) + settings.setdefault( + "sherpa_onnx_decoding_method", + asr.get("sherpa_onnx_decoding_method", "greedy_search"), + ) + settings.setdefault( + "sherpa_onnx_left_padding_seconds", + asr.get("sherpa_onnx_left_padding_seconds", 0.3), + ) + settings.setdefault( + "sherpa_onnx_tail_padding_seconds", + asr.get("sherpa_onnx_tail_padding_seconds", 0.5), + ) settings.setdefault("asr_device", asr.get("device", "cuda")) settings.setdefault( "sensevoice_pad_seconds", asr.get("sensevoice_pad_seconds", 0.5) From 928ddf15eb4755a1792a43f0473fe305bebc00f1 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sun, 21 Jun 2026 02:54:59 +0800 Subject: [PATCH 10/13] =?UTF-8?q?feat(vad):=20=E6=94=AF=E6=8C=81=20FireRed?= =?UTF-8?q?VAD=20=E5=8F=AF=E9=80=89=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FireRedVAD streaming rolling-frame 适配器,将 32ms float32 音频块转换为 25ms/10ms frame,并按 int16 幅度尺度送入 detect_frame - 在 VADProcessor 中加入 firered 模式,FireRedVAD 只提供 confidence,分段、静音、回溯切分、增量 ASR 和 ASR queue 仍复用现有状态机 - 增加 FireRedVAD Stream-VAD 本地模型发现、设置默认值、控制面板模型选择/刷新/状态显示和中英文 UI 文案 - 更新 pyproject/uv.lock、安装脚本可选模型下载、README/CLAUDE 使用说明,并修复 main.py 中字幕穿透设置保存的未定义函数引用 --- CLAUDE.md | 8 +- README.md | 28 ++++++- README_zh.md | 28 ++++++- config.yaml | 7 ++ control_panel.py | 178 +++++++++++++++++++++++++++++++++++++++++--- i18n/en.yaml | 14 ++++ i18n/zh.yaml | 14 ++++ install.ps1 | 65 +++++++++++++++- main.py | 2 +- model_manager.py | 183 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + settings_store.py | 14 ++++ uv.lock | 51 +++++++++++++ vad_firered.py | 145 ++++++++++++++++++++++++++++++++++++ vad_processor.py | 128 ++++++++++++++++++++++++++++++-- 15 files changed, 836 insertions(+), 30 deletions(-) create mode 100644 vad_firered.py diff --git a/CLAUDE.md b/CLAUDE.md index 5695a49..a8ba486 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,8 @@ main.py (LiveTranslateApp) |-- model_manager.py Centralized model detection, download, cache utils |-- pipeline_controller.py Audio capture + VAD + ASR queue controller, incremental ASR coordination |-- audio_capture.py WASAPI loopback via pyaudiowpatch, auto-reconnects on device change - |-- vad_processor.py Silero VAD / energy-based / disabled modes, progressive silence + backtrack split + |-- vad_processor.py Silero VAD / FireRedVAD / energy-based / disabled modes, progressive silence + backtrack split + |-- vad_firered.py FireRedVAD streaming rolling-frame confidence adapter |-- asr_client.py Main-process ASR worker manager (spawn, Pipe IPC, timeouts) |-- asr_worker.py ASR subprocess entrypoint; loads and owns one backend/model |-- asr_engine.py faster-whisper (Whisper) backend @@ -149,6 +150,7 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr ### VAD Behavior +- **FireRedVAD mode**: `vad_firered.py` lazy-imports `fireredvad` and adapts LiveTranslate 32ms float32 chunks into FireRedVAD 25ms/10ms streaming frames. FireRedVAD only supplies speech confidence; `VADProcessor` still owns segmentation, silence handling, backtrack splitting, incremental ASR buffer state, and ASR queue contract. - **Progressive silence**: Buffer越长接受越短的停顿切分 (<3s=full, 3-6s=half, 6-10s=quarter of silence_limit) - **Adaptive silence**: Tracks recent pause durations, sets threshold to P75 × 1.2, auto-adjusts between 0.3s~2.0s - **Backtrack split**: Max duration时回溯smoothed confidence history找最低谷切分,remainder保留到下一段 @@ -168,7 +170,9 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr - FunASR Nano: `asr_funasr_nano.py` does `os.chdir(model_dir)` before `AutoModel()` inside the ASR worker process, so relative paths in config.yaml (e.g. `Qwen3-0.6B`) resolve locally instead of triggering HuggingFace Hub network requests, without changing the GUI process cwd - `Translator` defaults to 10s timeout via `make_openai_client()` to prevent API calls from hanging indefinitely - Log window is created at startup but hidden; shown via tray menu "Show Log" -- Audio chunk duration is 32ms (512 samples at 16kHz), matching Silero VAD's native window size for minimal latency +- Audio chunk duration is 32ms (512 samples at 16kHz), matching Silero VAD's native window size for minimal latency. FireRedVAD keeps this capture cadence and internally rolls 25ms windows with 10ms shifts. +- FireRedVAD model discovery expects the official `Stream-VAD` directory containing `cmvn.ark` and `model.pth.tar`; the model is optional and not part of first-launch required downloads. +- FireRedVAD input scaling is explicit: LiveTranslate keeps ASR/VAD buffers as float32 PCM, while `vad_firered.py` clips to [-1, 1] and multiplies by 32768 before calling `detect_frame()`. - FunASR `disable_pbar=True` required in all `generate()` calls — tqdm crashes in GUI process when flushing stderr - ASR engine lifecycle: the GUI process never instantiates `ASREngine`, `FunASREngine`, or `AnimeWhisperEngine` directly. It owns an `ASRClient`; each worker process owns one concrete backend/model. Engine/model/device changes shut down the current worker first, then start a new worker. On target load failure, the saved previous worker config is used to restore ASR. - CrispASR is treated as a ggml C++ runtime hub with GGUF/bin single-file weights. The GUI process never imports `crispasr`; `asr_crispasr.py` imports the Python binding only inside the ASR worker process and adapts results to the common ASR dict. diff --git a/README.md b/README.md index 446458a..078aa21 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Works with any system audio — videos, livestreams, voice chat. No player modif - **Streaming translation display**: Real-time character-by-character translation output - **Per-model settings**: Streaming, structured output (JSON), context history, disable thinking - **Microphone mix-in**: Optionally mix microphone input with system audio for ASR -- **Low-latency VAD**: 32ms chunks + Silero VAD with adaptive silence detection +- **Low-latency VAD**: 32ms chunks + Silero VAD or optional FireRedVAD with adaptive silence detection - **Transparent overlay**: Always-on-top, click-through, draggable, 14 color themes - **CUDA acceleration**: GPU-accelerated ASR inference - **Auto model management**: Setup wizard, ModelScope / HuggingFace dual sources @@ -89,6 +89,7 @@ uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" ``` > FunASR uses `--no-deps` because `editdistance` requires a C++ compiler. `editdistance-s` in `pyproject.toml` is a pure-Python drop-in replacement. +> `uv sync` installs the optional FireRedVAD Python package; its model is downloaded separately only if you choose to use that VAD backend. @@ -98,6 +99,25 @@ uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" 2. Silero VAD + SenseVoice models download automatically (~1GB) 3. Main UI appears when ready +## FireRedVAD Models + +FireRedVAD is optional and is not downloaded during first launch. To use it, download the official model under `models/FireRedVAD`: + +```powershell +modelscope download --model xukaituo/FireRedVAD --local_dir ./models/FireRedVAD +# or +huggingface-cli download FireRedTeam/FireRedVAD --local-dir ./models/FireRedVAD +``` + +Make sure these files exist: + +```text +models/FireRedVAD/Stream-VAD/cmvn.ark +models/FireRedVAD/Stream-VAD/model.pth.tar +``` + +Then open Settings → VAD/ASR, choose `FireRedVAD`, click Refresh, and select the local Stream-VAD model. The first integration uses FireRedVAD only as a streaming speech-confidence backend; LiveTranslate still owns segmentation, silence handling, backtrack splitting, incremental ASR, and ASR queueing. FireRed AED is not connected. + ## sherpa-onnx Models LiveTranslate supports sherpa-onnx local ONNX models through Python `OfflineRecognizer` and `OnlineRecognizer` APIs. Online models are currently decoded as a VAD-segment wrapper, not as true partial streaming ASR. `install.ps1` installs the CPU wheel by default. CUDA requires replacing it with a CUDA wheel, for example: @@ -124,14 +144,15 @@ Real API keys are stored in `user_settings.json`, which is git-ignored. Keep `co ## Architecture ``` -Audio (WASAPI 32ms) → VAD (Silero) → ASR → LLM Translation → Overlay +Audio (WASAPI 32ms) → VAD (Silero / FireRedVAD / Energy) → ASR → LLM Translation → Overlay ↑ optional mic mix-in ``` ``` main.py Entry point & pipeline ├── audio_capture.py WASAPI loopback + mic mix-in -├── vad_processor.py Silero VAD +├── vad_processor.py VAD state machine (Silero / FireRedVAD / energy / disabled) +├── vad_firered.py FireRedVAD streaming frame adapter ├── asr_engine.py faster-whisper backend ├── asr_funasr.py Unified FunASR model selector backend ├── asr_sensevoice.py SenseVoice backend @@ -156,6 +177,7 @@ main.py Entry point & pipeline - CrispASR — ggml C++ ASR runtime hub with GGUF/bin single-file models, used through its Python binding in the ASR worker - [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — ONNX ASR runtime used through `OfflineRecognizer` and segment-wrapped `OnlineRecognizer` - [Silero VAD](https://github.com/snakers4/silero-vad) — Voice activity detection +- [FireRedVAD](https://github.com/FireRedTeam/FireRedVAD) — Optional streaming VAD confidence backend ## Star History diff --git a/README_zh.md b/README_zh.md index 5cd7458..3edcc77 100644 --- a/README_zh.md +++ b/README_zh.md @@ -27,7 +27,7 @@ Windows 实时音频翻译工具。捕获系统音频(WASAPI loopback)和可 - **流式翻译显示**:翻译结果逐字实时显示 - **模型独立配置**:流式传输、结构化输出(JSON)、上下文历史、禁用思考 - **麦克风混音**:可选将麦克风输入混合到系统音频一起识别 -- **低延迟 VAD**:32ms 音频块 + Silero VAD,自适应静音检测 +- **低延迟 VAD**:32ms 音频块 + Silero VAD,可选 FireRedVAD,自适应静音检测 - **透明悬浮窗**:始终置顶、鼠标穿透、可拖拽,14 种配色主题 - **CUDA 加速**:ASR 模型 GPU 推理 - **模型自动管理**:首次启动向导,支持 ModelScope / HuggingFace 双源 @@ -89,6 +89,7 @@ uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" ``` > FunASR 使用 `--no-deps` 安装,因为 `editdistance` 需要 C++ 编译器。`pyproject.toml` 中已包含纯 Python 替代品 `editdistance-s`。 +> `uv sync` 会安装可选的 FireRedVAD Python 包;FireRedVAD 模型只在你选择使用该 VAD 后端时单独下载。 @@ -98,6 +99,25 @@ uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" 2. 自动下载 Silero VAD + SenseVoice 模型(约 1GB) 3. 下载完成后进入主界面 +## FireRedVAD 模型 + +FireRedVAD 是可选 VAD 后端,首次启动不会强制下载。使用前请把官方模型下载到 `models/FireRedVAD`: + +```powershell +modelscope download --model xukaituo/FireRedVAD --local_dir ./models/FireRedVAD +# 或 +huggingface-cli download FireRedTeam/FireRedVAD --local-dir ./models/FireRedVAD +``` + +确保存在: + +```text +models/FireRedVAD/Stream-VAD/cmvn.ark +models/FireRedVAD/Stream-VAD/model.pth.tar +``` + +然后在 设置 → VAD/ASR → VAD 模式 中选择 `FireRedVAD`,点击刷新并选择本地 Stream-VAD 模型。当前集成只把 FireRedVAD 用作 streaming speech confidence 后端;分段、静音、回溯切分、增量 ASR 和 ASR queue 仍由 LiveTranslate 的 VADProcessor 负责。FireRed AED 暂未接入。 + ## sherpa-onnx 模型 LiveTranslate 通过 Python `OfflineRecognizer` 和 `OnlineRecognizer` API 接入 sherpa-onnx 本地 ONNX 模型。Online 模型当前是对 VAD 切段做整段 segment-wrapper 解码,不是真正逐 chunk partial streaming ASR。`install.ps1` 默认安装 CPU wheel;CUDA 版需要用 CUDA wheel 替换,例如: @@ -124,14 +144,15 @@ powershell -ExecutionPolicy Bypass -File install.ps1 -SherpaOnnxRuntime cuda12 ## 架构 ``` -Audio (WASAPI 32ms) → VAD (Silero) → ASR → LLM Translation → Overlay +Audio (WASAPI 32ms) → VAD (Silero / FireRedVAD / Energy) → ASR → LLM Translation → Overlay ↑ 可选麦克风混音 ``` ``` main.py 主入口,管线编排 ├── audio_capture.py WASAPI loopback + 麦克风混音 -├── vad_processor.py Silero VAD +├── vad_processor.py VAD 状态机 (Silero / FireRedVAD / energy / disabled) +├── vad_firered.py FireRedVAD streaming frame 适配器 ├── asr_engine.py faster-whisper 后端 ├── asr_funasr.py 统一 FunASR 模型选择后端 ├── asr_sensevoice.py SenseVoice 后端 @@ -156,6 +177,7 @@ main.py 主入口,管线编排 - CrispASR — ggml C++ ASR runtime hub,使用 GGUF/bin 单文件模型,LiveTranslate 通过 ASR worker 内的 Python binding 调用 - [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — 通过 `OfflineRecognizer` 和 segment-wrapper `OnlineRecognizer` 调用的 ONNX ASR runtime - [Silero VAD](https://github.com/snakers4/silero-vad) — 语音活动检测 +- [FireRedVAD](https://github.com/FireRedTeam/FireRedVAD) — 可选 streaming VAD confidence 后端 ## Star History diff --git a/config.yaml b/config.yaml index 0c0a3b8..7e350a1 100644 --- a/config.yaml +++ b/config.yaml @@ -46,6 +46,13 @@ asr: max_speech_duration: 8.0 # VAD threshold (0.0 - 1.0) vad_threshold: 0.5 + # VAD mode: silero, firered, energy, disabled + vad_mode: "silero" + # FireRedVAD Stream-VAD model directory. Empty means not selected. + firered_vad_model: "" + firered_vad_use_gpu: false + firered_vad_smooth_window_size: 5 + firered_vad_frame_aggregation: "max" # SenseVoice input padding bucket in seconds (0 = disabled) sensevoice_pad_seconds: 0.5 # faster-whisper input padding bucket in seconds (0 = disabled) diff --git a/control_panel.py b/control_panel.py index cfe38f6..cd9c0a5 100644 --- a/control_panel.py +++ b/control_panel.py @@ -44,10 +44,12 @@ get_cache_entries, list_local_crispasr_models, list_local_faster_whisper_models, + list_local_firered_vad_models, list_local_sherpa_onnx_models, migrate_funasr_settings, normalize_funasr_model_key, resolve_custom_crispasr_model, + resolve_custom_firered_vad_model, resolve_custom_sherpa_onnx_model, resolve_custom_whisper_model, ) @@ -474,16 +476,24 @@ def _create_vad_tab(self): mode_group = QGroupBox(t("group_vad_mode")) mode_layout = QVBoxLayout(mode_group) self._vad_mode = QComboBox() - self._vad_mode.addItems([t("vad_silero"), t("vad_energy"), t("vad_disabled")]) - mode_map = {"silero": 0, "energy": 1, "disabled": 2} - self._vad_mode.setCurrentIndex(mode_map.get(s.get("vad_mode", "energy"), 1)) + for label, key in ( + (t("vad_silero"), "silero"), + (t("vad_firered"), "firered"), + (t("vad_energy"), "energy"), + (t("vad_disabled"), "disabled"), + ): + self._vad_mode.addItem(label, key) + mode_idx = self._vad_mode.findData(s.get("vad_mode", "silero")) + if mode_idx < 0: + mode_idx = self._vad_mode.findData("silero") + self._vad_mode.setCurrentIndex(mode_idx) self._vad_mode.currentIndexChanged.connect(self._on_vad_mode_changed) self._vad_mode.currentIndexChanged.connect(self._auto_save) mode_layout.addWidget(self._vad_mode) layout.addWidget(mode_group) - silero_group = QGroupBox(t("group_silero_threshold")) - silero_layout = QGridLayout(silero_group) + self._neural_vad_group = QGroupBox(t("group_neural_vad_threshold")) + silero_layout = QGridLayout(self._neural_vad_group) self._vad_threshold_slider = QSlider(Qt.Orientation.Horizontal) self._vad_threshold_slider.setRange(0, 100) vad_pct = int(s.get("vad_threshold", 0.5) * 100) @@ -495,10 +505,49 @@ def _create_vad_tab(self): silero_layout.addWidget(QLabel(t("label_threshold")), 0, 0) silero_layout.addWidget(self._vad_threshold_slider, 0, 1) silero_layout.addWidget(self._vad_threshold_label, 0, 2) - layout.addWidget(silero_group) - - energy_group = QGroupBox(t("group_energy_threshold")) - energy_layout = QGridLayout(energy_group) + layout.addWidget(self._neural_vad_group) + + self._firered_vad_group = QGroupBox(t("group_firered_vad")) + firered_layout = QGridLayout(self._firered_vad_group) + firered_layout.setColumnStretch(1, 1) + self._firered_vad_model_combo = QComboBox() + saved_firered_model = s.get("firered_vad_model", "") + self._populate_firered_vad_models(saved_firered_model) + self._firered_vad_model_combo.currentIndexChanged.connect( + self._on_firered_vad_model_changed + ) + self._firered_vad_status = QLabel("") + self._firered_vad_status.setStyleSheet("color: #888; font-size: 11px;") + self._firered_vad_refresh_btn = QPushButton( + t("btn_refresh_firered_vad_models") + ) + self._firered_vad_refresh_btn.clicked.connect( + self._refresh_firered_vad_models + ) + self._firered_vad_smooth_window = QSpinBox() + self._firered_vad_smooth_window.setRange(1, 30) + self._firered_vad_smooth_window.setValue( + int(s.get("firered_vad_smooth_window_size", 5) or 5) + ) + self._firered_vad_smooth_window.valueChanged.connect( + self._on_firered_vad_setting_changed + ) + self._firered_vad_use_gpu = QCheckBox(t("label_firered_vad_use_gpu")) + self._firered_vad_use_gpu.setChecked(bool(s.get("firered_vad_use_gpu", False))) + self._firered_vad_use_gpu.toggled.connect( + self._on_firered_vad_setting_changed + ) + firered_layout.addWidget(QLabel(t("label_firered_vad_model")), 0, 0) + firered_layout.addWidget(self._firered_vad_model_combo, 0, 1) + firered_layout.addWidget(self._firered_vad_refresh_btn, 0, 2) + firered_layout.addWidget(self._firered_vad_status, 1, 1, 1, 2) + firered_layout.addWidget(QLabel(t("label_firered_vad_smooth_window")), 2, 0) + firered_layout.addWidget(self._firered_vad_smooth_window, 2, 1) + firered_layout.addWidget(self._firered_vad_use_gpu, 3, 1) + layout.addWidget(self._firered_vad_group) + + self._energy_group = QGroupBox(t("group_energy_threshold")) + energy_layout = QGridLayout(self._energy_group) self._energy_slider = QSlider(Qt.Orientation.Horizontal) self._energy_slider.setRange(1, 100) energy_pm = int(s.get("energy_threshold", 0.03) * 1000) @@ -510,7 +559,7 @@ def _create_vad_tab(self): energy_layout.addWidget(QLabel(t("label_threshold")), 0, 0) energy_layout.addWidget(self._energy_slider, 0, 1) energy_layout.addWidget(self._energy_label, 0, 2) - layout.addWidget(energy_group) + layout.addWidget(self._energy_group) timing_group = QGroupBox(t("group_timing")) timing_layout = QGridLayout(timing_group) @@ -577,6 +626,8 @@ def _create_vad_tab(self): layout.addWidget(timing_group) + self._update_firered_vad_status() + self._update_vad_detail_visibility() layout.addStretch() return widget @@ -1168,6 +1219,10 @@ def _get_asr_lang_code(self) -> str: """Get the language code from the ASR language combo (stored as userData).""" return self._asr_lang.currentData() or "auto" + def _selected_vad_mode(self) -> str: + value = self._vad_mode.currentData() + return str(value) if value else "silero" + def _selected_asr_engine(self) -> str: value = self._asr_engine.currentData() return str(value) if value else "funasr" @@ -1291,10 +1346,88 @@ def _on_sherpa_onnx_setting_changed(self): ) self._auto_save() + def _selected_firered_vad_model(self) -> str: + value = self._firered_vad_model_combo.currentData() + return str(value) if value is not None else "" + + def _on_firered_vad_model_changed(self): + self._current_settings["firered_vad_model"] = ( + self._selected_firered_vad_model() + ) + self._update_firered_vad_status() + self._auto_save() + + def _on_firered_vad_setting_changed(self): + self._current_settings["firered_vad_smooth_window_size"] = ( + self._firered_vad_smooth_window.value() + ) + self._current_settings["firered_vad_use_gpu"] = ( + self._firered_vad_use_gpu.isChecked() + ) + self._auto_save() + def _selected_whisper_model(self) -> str: value = self._whisper_size_combo.currentData() return str(value) if value else self._whisper_size_combo.currentText() + def _populate_firered_vad_models(self, saved_value: str): + self._firered_vad_model_combo.clear() + self._firered_vad_model_combo.addItem(t("firered_vad_model_placeholder"), "") + + local_prefix = t("firered_vad_local_prefix") + for item in list_local_firered_vad_models(): + idx = self._firered_vad_model_combo.count() + self._firered_vad_model_combo.addItem( + f"{local_prefix}: {item['name']}", item["path"] + ) + self._firered_vad_model_combo.setItemData( + idx, item["path"], Qt.ItemDataRole.ToolTipRole + ) + + if not saved_value: + selected = "" + else: + selected = resolve_custom_firered_vad_model(saved_value) or saved_value + idx = self._firered_vad_model_combo.findData(selected) + if idx < 0: + idx = self._firered_vad_model_combo.findText(saved_value) + if idx < 0 and selected: + label = f"{t('firered_vad_missing_local')}: {Path(str(selected)).name}" + idx = self._firered_vad_model_combo.count() + self._firered_vad_model_combo.addItem(label, selected) + self._firered_vad_model_combo.setItemData( + idx, str(selected), Qt.ItemDataRole.ToolTipRole + ) + if idx >= 0: + self._firered_vad_model_combo.setCurrentIndex(idx) + + def _refresh_firered_vad_models(self): + saved = self._selected_firered_vad_model() + self._populate_firered_vad_models(saved) + self._update_firered_vad_status() + + def _update_firered_vad_status(self): + model_key = self._selected_firered_vad_model() + if not model_key: + if list_local_firered_vad_models(): + self._firered_vad_status.setText(t("firered_vad_select_model")) + else: + self._firered_vad_status.setText(t("firered_vad_no_local_models")) + self._firered_vad_status.setStyleSheet( + "color: #888; font-size: 11px;" + ) + return + if resolve_custom_firered_vad_model(model_key): + self._firered_vad_status.setText(t("firered_vad_local_ready")) + self._firered_vad_status.setStyleSheet( + "color: #4a4; font-size: 11px;" + ) + else: + self._firered_vad_status.setText(t("firered_vad_invalid_local")) + self._firered_vad_status.setStyleSheet( + "color: #d66; font-size: 11px;" + ) + def _populate_sherpa_onnx_models(self, saved_value: str): self._sherpa_onnx_model_combo.clear() self._sherpa_onnx_model_combo.addItem(t("sherpa_onnx_model_placeholder"), "") @@ -1664,8 +1797,19 @@ def _on_silence_mode_changed(self, index): self._silence_duration.setEnabled(index == 1) def _on_vad_mode_changed(self, index): - modes = ["silero", "energy", "disabled"] - self._current_settings["vad_mode"] = modes[index] + self._current_settings["vad_mode"] = self._selected_vad_mode() + self._update_vad_detail_visibility() + + def _update_vad_detail_visibility(self): + if not hasattr(self, "_vad_mode"): + return + mode = self._selected_vad_mode() + if hasattr(self, "_neural_vad_group"): + self._neural_vad_group.setVisible(mode in ("silero", "firered")) + if hasattr(self, "_firered_vad_group"): + self._firered_vad_group.setVisible(mode == "firered") + if hasattr(self, "_energy_group"): + self._energy_group.setVisible(mode == "energy") def _on_threshold_changed(self, value): val = value / 100.0 @@ -1745,6 +1889,16 @@ def _apply_prompt(self): def _apply_settings(self): self._current_settings["asr_language"] = self._get_asr_lang_code() + self._current_settings["vad_mode"] = self._selected_vad_mode() + self._current_settings["firered_vad_model"] = ( + self._selected_firered_vad_model() + ) + self._current_settings["firered_vad_smooth_window_size"] = ( + self._firered_vad_smooth_window.value() + ) + self._current_settings["firered_vad_use_gpu"] = ( + self._firered_vad_use_gpu.isChecked() + ) self._current_settings["asr_engine"] = self._selected_asr_engine() self._current_settings["funasr_model"] = self._selected_funasr_model() if hasattr(self, "_remote_url_edit"): diff --git a/i18n/en.yaml b/i18n/en.yaml index 8a7275c..c71d892 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -107,10 +107,24 @@ hub_huggingface: "HuggingFace (Intl)" label_ui_lang: "Language:" group_vad_mode: "VAD Mode" vad_silero: "Silero VAD" +vad_firered: "FireRedVAD" vad_energy: "Energy-based" vad_disabled: "Disabled (always send)" group_silero_threshold: "Silero VAD Threshold" +group_neural_vad_threshold: "Neural VAD Threshold" label_threshold: "Threshold:" +group_firered_vad: "FireRedVAD" +label_firered_vad_model: "FireRedVAD model:" +btn_refresh_firered_vad_models: "Refresh" +firered_vad_model_placeholder: "Select FireRedVAD Stream-VAD model" +firered_vad_select_model: "Select a model" +firered_vad_no_local_models: "No local FireRedVAD models found" +firered_vad_local_prefix: "Local" +firered_vad_local_ready: "Local model" +firered_vad_missing_local: "Missing" +firered_vad_invalid_local: "Local model unavailable" +label_firered_vad_smooth_window: "Smooth window:" +label_firered_vad_use_gpu: "Use GPU" group_energy_threshold: "Energy Threshold (for Energy-based mode)" group_timing: "Timing" label_min_speech: "Min speech:" diff --git a/i18n/zh.yaml b/i18n/zh.yaml index cb060e7..d7528b9 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -107,10 +107,24 @@ hub_huggingface: "HuggingFace (国际)" label_ui_lang: "界面语言:" group_vad_mode: "VAD 模式" vad_silero: "Silero VAD" +vad_firered: "FireRedVAD" vad_energy: "能量检测" vad_disabled: "禁用 (始终发送)" group_silero_threshold: "Silero VAD 阈值" +group_neural_vad_threshold: "神经 VAD 阈值" label_threshold: "阈值:" +group_firered_vad: "FireRedVAD" +label_firered_vad_model: "FireRedVAD 模型:" +btn_refresh_firered_vad_models: "刷新" +firered_vad_model_placeholder: "请选择 FireRedVAD Stream-VAD 模型" +firered_vad_select_model: "请选择模型" +firered_vad_no_local_models: "未扫描到本地 FireRedVAD 模型" +firered_vad_local_prefix: "本地" +firered_vad_local_ready: "本地模型" +firered_vad_missing_local: "缺失" +firered_vad_invalid_local: "本地模型不可用" +label_firered_vad_smooth_window: "平滑窗口:" +label_firered_vad_use_gpu: "使用 GPU" group_energy_threshold: "能量阈值 (用于能量检测模式)" group_timing: "时间参数" label_min_speech: "最短语音:" diff --git a/install.ps1 b/install.ps1 index ec24b70..0d7379a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -3,7 +3,12 @@ param( [ValidateSet("cpu", "cuda11", "cuda12")] - [string]$SherpaOnnxRuntime = "cpu" + [string]$SherpaOnnxRuntime = "cpu", + + [switch]$DownloadFireRedVAD, + + [ValidateSet("ms", "hf")] + [string]$FireRedVADHub = "ms" ) $ErrorActionPreference = "Stop" @@ -98,6 +103,51 @@ function Install-SherpaOnnxRuntime { } } +function Test-FireRedVadPackage { + param([string]$PythonExe) + + Write-Step "Checking FireRedVAD Python package..." + try { + $version = & $PythonExe -c "import importlib.metadata as m; import fireredvad; print(m.version('fireredvad'))" 2>&1 + if ($LASTEXITCODE -ne 0) { throw $version } + Write-Ok "fireredvad $($version.Trim())" + } catch { + Write-Warn "FireRedVAD package check failed: $($_.Exception.Message)" + Write-Warn "FireRedVAD VAD mode will be unavailable until fireredvad is installed" + } +} + +function Download-FireRedVADModel { + param( + [string]$PythonExe, + [string]$Hub + ) + + Write-Step "Downloading FireRedVAD Stream-VAD model..." + $target = Join-Path $ProjectDir "models\FireRedVAD" + New-Item -ItemType Directory -Force -Path $target | Out-Null + try { + if ($Hub -eq "ms") { + Write-Host " Source: ModelScope xukaituo/FireRedVAD" -ForegroundColor Gray + & $PythonExe -c "from modelscope import snapshot_download; snapshot_download(model_id='xukaituo/FireRedVAD', local_dir=r'models/FireRedVAD')" + } else { + Write-Host " Source: HuggingFace FireRedTeam/FireRedVAD" -ForegroundColor Gray + & $PythonExe -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='FireRedTeam/FireRedVAD', local_dir=r'models/FireRedVAD')" + } + if ($LASTEXITCODE -ne 0) { throw "FireRedVAD model download command failed" } + + $cmvn = Join-Path $target "Stream-VAD\cmvn.ark" + $model = Join-Path $target "Stream-VAD\model.pth.tar" + if (-not (Test-Path $cmvn) -or -not (Test-Path $model)) { + throw "Downloaded model is missing Stream-VAD\cmvn.ark or Stream-VAD\model.pth.tar" + } + Write-Ok "FireRedVAD model ready: models\FireRedVAD\Stream-VAD" + } catch { + Write-Warn "FireRedVAD model download failed: $($_.Exception.Message)" + Write-Warn "You can download it later and place it under models\FireRedVAD" + } +} + function Enable-SystemProxy { # uv (Python download) and pip honor *_PROXY env vars but not the Windows # registry system proxy; bridge it here. An already-set env proxy wins. @@ -409,13 +459,20 @@ if (Test-Path ".\repair_torch_metadata.ps1") { & powershell -NoProfile -ExecutionPolicy Bypass -File ".\repair_torch_metadata.ps1" -PythonExe $Python } -# ── Step 6: Install CrispASR native runtime ── +# ── Step 6: Verify FireRedVAD dependency ── +Test-FireRedVadPackage -PythonExe $Python + +if ($DownloadFireRedVAD) { + Download-FireRedVADModel -PythonExe $Python -Hub $FireRedVADHub +} + +# ── Step 7: Install CrispASR native runtime ── Install-CrispAsrNativeRuntime -PythonExe $Python -UseCuda $HasNvidia -# ── Step 7: Install sherpa-onnx runtime ── +# ── Step 8: Install sherpa-onnx runtime ── Install-SherpaOnnxRuntime -PythonExe $Python -Runtime $SherpaOnnxRuntime -# ── Step 8: Install FunASR (no-deps) ── +# ── Step 9: Install FunASR (no-deps) ── Write-Step "Installing FunASR (--no-deps)..." & $Uv pip install --python $Python funasr --no-deps diff --git a/main.py b/main.py index 3bc7532..d8c5489 100644 --- a/main.py +++ b/main.py @@ -1052,7 +1052,7 @@ def on_toggle_subwin_ct(checked): sm["click_through"] = checked settings["subtitle_mode"] = sm panel._current_settings["subtitle_mode"] = sm - _save_settings(settings) + save_settings(settings) w = panel._subtitle_widget w._click_through_check.blockSignals(True) w._click_through_check.setChecked(checked) diff --git a/model_manager.py b/model_manager.py index 58fc544..413c61a 100644 --- a/model_manager.py +++ b/model_manager.py @@ -108,6 +108,7 @@ def _proxy_env(proxy: str): DEFAULT_FUNASR_MODEL = "sensevoice-small" DEFAULT_SHERPA_ONNX_MODEL = "" +DEFAULT_FIRERED_VAD_MODEL = "" FUNASR_LEGACY_ENGINE_ALIASES = { "sensevoice": "sensevoice-small", @@ -151,6 +152,7 @@ def asr_model_id( _MODEL_SIZE_BYTES = { "silero-vad": 2_000_000, + "firered-vad": 2_200_000, "sensevoice": 940_000_000, "funasr-nano": 1_050_000_000, "funasr-mlt-nano": 1_050_000_000, @@ -689,6 +691,184 @@ def get_sherpa_onnx_model_path(value) -> str | None: return resolve_custom_sherpa_onnx_model(value) +def _custom_firered_vad_path(value) -> Path | None: + if not value: + return None + path = Path(str(value)).expanduser() + if not path.is_absolute(): + path = APP_DIR / path + return path + + +def _read_firered_vad_metadata(path: Path) -> dict: + for name in ("firered_vad_model.json", "model.json"): + metadata_path = path / name + if not metadata_path.is_file(): + continue + try: + data = json.loads(metadata_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + log.warning(f"Invalid FireRedVAD metadata: {metadata_path}: {exc}") + return {} + return data if isinstance(data, dict) else {} + return {} + + +def _firered_family_hint(metadata: dict) -> str: + family = str(metadata.get("family") or "").strip().lower().replace("-", "_") + if family in ("stream_vad", "streamvad", "firered_stream_vad"): + return "stream_vad" + return "" + + +def _is_firered_stream_files(path: Path) -> bool: + return (path / "cmvn.ark").is_file() and (path / "model.pth.tar").is_file() + + +def _is_stream_vad_name(path: Path) -> bool: + name = path.name.lower().replace("_", "-") + return "stream-vad" in name or name == "streamvad" + + +def _firered_display_name(path: Path, metadata: dict) -> str: + display_name = metadata.get("display_name") or metadata.get("name") + if display_name: + return str(display_name).strip() + + repo_name = _hf_snapshot_name(path) or _hf_snapshot_name(path.parent) + if repo_name: + return f"{repo_name} / {path.name}" + if _is_stream_vad_name(path) and path.parent != MODELS_DIR: + return f"{path.parent.name} / {path.name}" + return path.name + + +def detect_firered_vad_model_dir(path) -> dict | None: + """Return normalized FireRedVAD Stream-VAD metadata for a usable directory.""" + if not path: + return None + path = Path(path) + if not path.is_dir(): + return None + try: + path = path.resolve() + except OSError: + return None + + root_metadata = _read_firered_vad_metadata(path) + root_family = _firered_family_hint(root_metadata) + + candidate: Path | None = None + candidate_metadata: dict = {} + + if _is_firered_stream_files(path) and ( + _is_stream_vad_name(path) or root_family == "stream_vad" + ): + candidate = path + candidate_metadata = root_metadata + + if candidate is None: + model_dir = root_metadata.get("model_dir") + if model_dir: + child = Path(str(model_dir)) + if not child.is_absolute(): + child = path / child + if child.is_dir() and _is_firered_stream_files(child): + candidate = child.resolve() + candidate_metadata = { + **root_metadata, + **_read_firered_vad_metadata(candidate), + } + + if candidate is None: + for child_name in ("Stream-VAD", "stream-vad", "Stream_VAD", "stream_vad"): + child = path / child_name + if child.is_dir() and _is_firered_stream_files(child): + candidate = child.resolve() + candidate_metadata = { + **root_metadata, + **_read_firered_vad_metadata(candidate), + } + break + + if candidate is None: + return None + + return { + "name": _firered_display_name(candidate, candidate_metadata), + "path": str(candidate), + "family": "stream_vad", + "display_name": _firered_display_name(candidate, candidate_metadata), + } + + +def is_firered_vad_stream_model_dir(path) -> bool: + return detect_firered_vad_model_dir(path) is not None + + +def resolve_custom_firered_vad_model(value) -> str | None: + path = _custom_firered_vad_path(value) + if not path: + return None + info = detect_firered_vad_model_dir(path) + if info: + return info["path"] + return None + + +def list_local_firered_vad_models() -> list[dict]: + """Scan ./models recursively for local FireRedVAD Stream-VAD models.""" + if not MODELS_DIR.exists(): + return [] + + candidates: set[Path] = set() + try: + for marker in MODELS_DIR.rglob("cmvn.ark"): + if not marker.is_file(): + continue + model_dir = marker.parent + candidates.add(model_dir) + if _is_stream_vad_name(model_dir): + candidates.add(model_dir.parent) + except (OSError, PermissionError): + return [] + + entries = [] + name_counts = {} + seen = set() + for candidate in sorted(candidates, key=lambda item: str(item).lower()): + info = detect_firered_vad_model_dir(candidate) + if not info: + continue + identity = info["path"] + if identity in seen: + continue + seen.add(identity) + name = info["display_name"] + name_counts[name] = name_counts.get(name, 0) + 1 + if name_counts[name] > 1: + name = f"{name} ({Path(identity).parent.name})" + entries.append({"name": name, "path": identity, "family": "stream_vad"}) + + entries.sort(key=lambda item: item["name"].lower()) + return entries + + +def get_firered_vad_model_path(value) -> str | None: + return resolve_custom_firered_vad_model(value) + + +def firered_vad_display_name(path) -> str | None: + resolved = resolve_custom_firered_vad_model(path) + if not resolved: + return None + for item in list_local_firered_vad_models(): + if item["path"] == resolved: + return item["name"] + info = detect_firered_vad_model_dir(resolved) + return info["display_name"] if info else Path(resolved).name + + def apply_cache_env(): """Point all model caches to ./models/.""" resolved = str(MODELS_DIR.resolve()) @@ -1060,6 +1240,9 @@ def get_cache_entries(): for item in list_local_sherpa_onnx_models(): entries.append((f"sherpa-onnx Local: {item['name']}", Path(item["path"]))) + for item in list_local_firered_vad_models(): + entries.append((f"FireRedVAD Local: {item['name']}", Path(item["path"]))) + if torch_base.exists(): for d in sorted(torch_base.glob("snakers4_silero-vad*")): if d.is_dir(): diff --git a/pyproject.toml b/pyproject.toml index b899a3e..ff53dde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "tiktoken>=0.7.0", "transformers>=4.40.0", "silero-vad>=5.0", + "fireredvad>=0.0.2,<0.1", "modelscope>=1.20.0", "huggingface_hub>=0.20.0", "PyQt6>=6.5.0", diff --git a/settings_store.py b/settings_store.py index ed7f0d7..8d41189 100644 --- a/settings_store.py +++ b/settings_store.py @@ -4,6 +4,7 @@ from typing import Any from model_manager import ( + DEFAULT_FIRERED_VAD_MODEL, DEFAULT_FUNASR_MODEL, migrate_funasr_settings, normalize_funasr_model_key, @@ -25,6 +26,19 @@ def normalize_settings(config: dict[str, Any], saved: dict | None = None) -> dic settings.setdefault("vad_mode", "silero") settings.setdefault("vad_threshold", asr.get("vad_threshold", 0.5)) settings.setdefault("energy_threshold", 0.02) + settings.setdefault( + "firered_vad_model", + asr.get("firered_vad_model", DEFAULT_FIRERED_VAD_MODEL), + ) + settings.setdefault("firered_vad_use_gpu", asr.get("firered_vad_use_gpu", False)) + settings.setdefault( + "firered_vad_smooth_window_size", + asr.get("firered_vad_smooth_window_size", 5), + ) + settings.setdefault( + "firered_vad_frame_aggregation", + asr.get("firered_vad_frame_aggregation", "max"), + ) settings.setdefault("min_speech_duration", asr.get("min_speech_duration", 1.0)) settings.setdefault("max_speech_duration", asr.get("max_speech_duration", 8.0)) settings.setdefault("silence_mode", "auto") diff --git a/uv.lock b/uv.lock index 27a6a14..5145fe0 100644 --- a/uv.lock +++ b/uv.lock @@ -354,6 +354,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, ] +[[package]] +name = "fireredvad" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "kaldi-native-fbank" }, + { name = "kaldiio" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "soundfile" }, + { name = "textgrid" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/40/c2c2fbfcbf106fc5d12cb4526adaf08a2c2449d88af757dd542dd98caca6/fireredvad-0.0.2.tar.gz", hash = "sha256:63e2fecfcf8551a4700390b952c67bfa3b1aece56991582cd12f8a020969404a", size = 25057, upload-time = "2026-03-09T12:25:22.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/94/e7764e16d00ef6ac775f5431b623ba8cb6d650aa7ea033ac09d6c346236e/fireredvad-0.0.2-py3-none-any.whl", hash = "sha256:7aca392e5d6ab22add80f5920561928bf2cd4adc7474b6d20952257941a11cf5", size = 29624, upload-time = "2026-03-09T12:25:20.887Z" }, +] + [[package]] name = "flatbuffers" version = "25.12.19" @@ -564,6 +581,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "kaldi-native-fbank" +version = "1.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/2c/84076b352107ce12d56f28c313f1aca1be332d953dd96aec7b84976e6d53/kaldi-native-fbank-1.22.3.tar.gz", hash = "sha256:387bf87225c6b83c93ae652eeaef1b4d531994b6e398e7a77189de340674f9af", size = 71013, upload-time = "2025-10-09T02:31:21.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/c1/ff7a8c85a100dbef0df6473579cb78b1527f01d34859432de3f38d5a38d1/kaldi_native_fbank-1.22.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:af04cae53beb6da1e28e57e053d16118513e5fbe8d16ce0b3261f1b1b396af0a", size = 244533, upload-time = "2025-10-09T02:28:39.795Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/be771230ba2f071ad036a9224139fa4e0ac576b8abac15209342fc63ef86/kaldi_native_fbank-1.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:947cb8fae3611244b15006bd42263fe56afe583077e7006aac4bdb0e10dc5a4f", size = 227900, upload-time = "2025-10-09T02:33:05.679Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9a/2ba3bcdf8b0d78339d3bb17307f70113d32bfc5492917d254995e61fd1c0/kaldi_native_fbank-1.22.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1e6b8c587dfe4f646b3a62f4b4ee840a83f4c491fe4bdfb74ccd9937ce17cdc", size = 296954, upload-time = "2025-10-09T02:30:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/9031a517a0655e54c5bb8f243078798c19441f037b70a3f10b8aad82d073/kaldi_native_fbank-1.22.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c710b62442a43720db853cbafbf57ea3f920b593128dfb8fb88e08d6a8225772", size = 320031, upload-time = "2025-10-09T02:30:45.244Z" }, + { url = "https://files.pythonhosted.org/packages/3e/14/3f0d13909fee89d3826279a129bc3261c677fb1f39db1c4f714d92918762/kaldi_native_fbank-1.22.3-cp310-cp310-win32.whl", hash = "sha256:4d3c97ee08b9d3d528ff4fe8a20aeb7484eea06b2ccb7a3b5a7c86fa3b065b44", size = 272314, upload-time = "2025-10-09T02:30:24.4Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/0be9a61d373449d9782dad3b259892720bdc90c553cf618cb45a1c6443c0/kaldi_native_fbank-1.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:1eb9a3a9c87597872a48acc167de7321b4660bc3a10ed2f552c5f633015b1b61", size = 302723, upload-time = "2025-10-09T02:28:31.64Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d0/07ab65d7c8389f56f8c772a55f8846a81c24d973abecfc0275c2c833f63e/kaldi_native_fbank-1.22.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:6b9ef5b6302ee45628a51a4484cb4f41006af02141508939c09ce36899fb3f41", size = 245879, upload-time = "2025-10-09T02:28:04.7Z" }, + { url = "https://files.pythonhosted.org/packages/64/2b/3132083b930fa6411f14469f36c465b7d2fba29a8a3e121d8fd6baffc8ea/kaldi_native_fbank-1.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29452f2900e771086e9022dde17a92d191217ab3e34ca7dc361bd9be53e94fb4", size = 229180, upload-time = "2025-10-09T02:29:35.356Z" }, + { url = "https://files.pythonhosted.org/packages/e3/53/720ffbe8b30de203570f397866334eb4c6364c9214699010f2086de911ff/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48e5dd8e897bf4509be2c6eeb4bbab728eaaef1f214ae0510c96219c4253d17", size = 299054, upload-time = "2025-10-09T02:28:42.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/3f/beb161e4fdf6710938ccf18418c147d87ba8f102903d6c6e4eda25588e22/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce84c65779c9eed6ec02699797a4ba1859451977537a993be3ea8167a210ec3e", size = 321921, upload-time = "2025-10-09T02:31:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bb/ee42418b77dbfc5ff619857b8eb372af98a88d47c8ca8b9a2d3ca2936c96/kaldi_native_fbank-1.22.3-cp311-cp311-win32.whl", hash = "sha256:516bce595eb5e5899a91dfec1142bea56a2fa232e53425e9966785aee8cd024e", size = 273018, upload-time = "2025-10-09T02:30:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/da630b035cd343311168e5fe02c39fe7b192638717e3202de92ccf8ae18e/kaldi_native_fbank-1.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:bd225d0624d45b533c1780094b3c59666276a6e9f20222943441212cdf301c9e", size = 303342, upload-time = "2025-10-09T02:28:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/c2/de/fbdbfcc75fad9d9a6f9a250bc986f1002902581eaa47a5948f53a7f11851/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7f636ccdea28bd187f93b06a1e4b9275e42e43af9405b0684fc739e829299c4b", size = 249003, upload-time = "2025-10-09T02:29:48.509Z" }, + { url = "https://files.pythonhosted.org/packages/77/64/e57ce185dda028b7b9af72cdfb16825bfa52183653945681e7cb8e7c2dfa/kaldi_native_fbank-1.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:abd31a8bfe1db62a7ddb0beee84f3a5de9bb559fcdd2b96ca0fb729c551b9412", size = 228933, upload-time = "2025-10-09T02:31:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/6f4fd8953c0b3f30de4526fd024095032abcdc25b6736c77a891687c604e/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5a44b4a83cf9bf13d3f77858928068b06d3ec2238c27ff2e39393fbf7749c9f", size = 298887, upload-time = "2025-10-09T02:30:53.739Z" }, + { url = "https://files.pythonhosted.org/packages/84/90/01ef7331c52b1eaf9916f3f7a535155aac2e9e2ddad12a141613d92758c7/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f16e74372fe9e20abb4183f98a8e2288d5ee4c48d04d94b6160311170e007661", size = 322002, upload-time = "2025-10-09T02:30:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/fce142bd3aeadb1292360a90ceb91f923c8e12081c21576fe69917243c5f/kaldi_native_fbank-1.22.3-cp312-cp312-win32.whl", hash = "sha256:a90f51377569575fc0d1a66ef7e89a36102bfb6dcd1d15d6c4afb930ce726672", size = 273308, upload-time = "2025-10-09T02:29:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c0b0b6280edabad85d7e15093fad612c027e175fe4e0b960ce2f36485143/kaldi_native_fbank-1.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:cbbeea19fe6d584c54e93fe6615a7185b10e0d78fdb6471f9e44596018437c38", size = 308023, upload-time = "2025-10-09T02:28:43.909Z" }, +] + [[package]] name = "kaldiio" version = "2.18.1" @@ -624,6 +667,7 @@ dependencies = [ { name = "crispasr" }, { name = "editdistance-s" }, { name = "faster-whisper" }, + { name = "fireredvad" }, { name = "httpx" }, { name = "huggingface-hub" }, { name = "hydra-core" }, @@ -655,6 +699,7 @@ requires-dist = [ { name = "crispasr", url = "https://github.com/CrispStrobe/CrispASR/archive/refs/tags/v0.7.2.zip", subdirectory = "python" }, { name = "editdistance-s", specifier = ">=1.0.0" }, { name = "faster-whisper", specifier = ">=1.0.0" }, + { name = "fireredvad", specifier = ">=0.0.2,<0.1" }, { name = "httpx", specifier = ">=0.28.0" }, { name = "huggingface-hub", specifier = ">=0.20.0" }, { name = "hydra-core", specifier = ">=1.3.0" }, @@ -1860,6 +1905,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "textgrid" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/6f/701ef6aa56cf85c8965b7ff929f0766e0e8311c4478937eeee9441bf9663/TextGrid-1.6.1.tar.gz", hash = "sha256:0d3f8d4f511474777ce287d2ecef090573c0e63141aa73c6f400637e1ecaca63", size = 9422, upload-time = "2024-02-29T19:32:14.729Z" } + [[package]] name = "threadpoolctl" version = "3.6.0" diff --git a/vad_firered.py b/vad_firered.py new file mode 100644 index 0000000..c79ef78 --- /dev/null +++ b/vad_firered.py @@ -0,0 +1,145 @@ +import logging +from pathlib import Path + +import numpy as np + +log = logging.getLogger("LiveTranslate.FireRedVAD") + +_SAMPLE_RATE = 16000 +_FRAME_LENGTH_SAMPLE = 400 # 25ms at 16kHz +_FRAME_SHIFT_SAMPLE = 160 # 10ms at 16kHz +_VALID_AGGREGATIONS = {"max", "latest", "mean"} + + +class FireRedVadAdapter: + """Rolling-frame adapter for FireRedVAD streaming confidence.""" + + def __init__( + self, + model_dir: str, + threshold: float = 0.5, + smooth_window_size: int = 5, + use_gpu: bool = False, + frame_aggregation: str = "max", + ): + self.model_dir = str(model_dir) + self.threshold = float(threshold) + self.smooth_window_size = max(1, int(smooth_window_size)) + self.use_gpu = bool(use_gpu) + self.frame_aggregation = str(frame_aggregation or "max").lower() + if self.frame_aggregation not in _VALID_AGGREGATIONS: + log.warning( + "Unknown FireRedVAD frame aggregation '%s', using max", + self.frame_aggregation, + ) + self.frame_aggregation = "max" + + model_path = Path(self.model_dir) + if not model_path.is_dir(): + raise RuntimeError( + f"FireRedVAD model directory does not exist: {model_path}. " + "Download FireRedTeam/FireRedVAD and select its Stream-VAD directory." + ) + missing = [ + name + for name in ("cmvn.ark", "model.pth.tar") + if not (model_path / name).is_file() + ] + if missing: + raise RuntimeError( + f"Invalid FireRedVAD Stream-VAD model directory: {model_path}. " + f"Missing: {', '.join(missing)}." + ) + + try: + from fireredvad import FireRedStreamVad, FireRedStreamVadConfig + except Exception as exc: + raise RuntimeError( + "FireRedVAD Python package is not available. " + "Install project dependencies or run: uv pip install fireredvad>=0.0.2" + ) from exc + + try: + config = FireRedStreamVadConfig( + use_gpu=self.use_gpu, + smooth_window_size=self.smooth_window_size, + speech_threshold=self.threshold, + ) + self._stream_vad = FireRedStreamVad.from_pretrained( + str(model_path), config + ) + except Exception as exc: + raise RuntimeError( + f"Failed to load FireRedVAD Stream-VAD model from {model_path}: {exc}" + ) from exc + + self._raw_buffer = np.empty(0, dtype=np.float32) + self._last_confidence = 0.0 + log.info( + "FireRedVAD loaded: model=%s, device=%s, smooth_window=%s, aggregation=%s", + model_path, + "gpu" if self.use_gpu else "cpu", + self.smooth_window_size, + self.frame_aggregation, + ) + + def confidence_for_chunk(self, audio_chunk: np.ndarray) -> float: + if self._stream_vad is None: + return self._last_confidence + + chunk = np.asarray(audio_chunk, dtype=np.float32).reshape(-1) + if chunk.size == 0: + return self._last_confidence + + self._raw_buffer = np.concatenate((self._raw_buffer, chunk)) + confidences: list[float] = [] + + while self._raw_buffer.size >= _FRAME_LENGTH_SAMPLE: + frame = self._raw_buffer[:_FRAME_LENGTH_SAMPLE] + self._raw_buffer = self._raw_buffer[_FRAME_SHIFT_SAMPLE:] + firered_frame = ( + np.clip(frame, -1.0, 1.0).astype(np.float32, copy=False) * 32768.0 + ) + try: + result = self._stream_vad.detect_frame(firered_frame) + except Exception: + log.exception("FireRedVAD detect_frame failed") + raise + confidence = getattr(result, "smoothed_prob", None) + if confidence is None: + confidence = getattr(result, "raw_prob", 0.0) + confidences.append(float(confidence)) + + if not confidences: + return self._last_confidence + + if self.frame_aggregation == "latest": + confidence = confidences[-1] + elif self.frame_aggregation == "mean": + confidence = float(sum(confidences) / len(confidences)) + else: + confidence = max(confidences) + + self._last_confidence = max(0.0, min(1.0, confidence)) + return self._last_confidence + + def reset(self): + self._raw_buffer = np.empty(0, dtype=np.float32) + self._last_confidence = 0.0 + if self._stream_vad is not None: + try: + self._stream_vad.reset() + except Exception: + log.warning("FireRedVAD reset failed", exc_info=True) + + def unload(self): + self._stream_vad = None + self._raw_buffer = np.empty(0, dtype=np.float32) + self._last_confidence = 0.0 + if self.use_gpu: + try: + import torch + + torch.cuda.empty_cache() + except Exception: + pass diff --git a/vad_processor.py b/vad_processor.py index a72e068..cd73a94 100644 --- a/vad_processor.py +++ b/vad_processor.py @@ -26,7 +26,14 @@ def __init__( self.min_speech_samples = int(min_speech_duration * sample_rate) self.max_speech_samples = int(max_speech_duration * sample_rate) self._chunk_duration = chunk_duration - self.mode = "silero" # "silero", "energy", "disabled" + self.mode = "silero" # "silero", "firered", "energy", "disabled" + self._firered = None + self._firered_model = "" + self._firered_use_gpu = False + self._firered_smooth_window_size = 5 + self._firered_frame_aggregation = "max" + self._firered_failed_key = None + self._firered_missing_warned_for = None # Silero v5 ships its model inside the `silero-vad` PyPI package, so load # it from there (zero network). Only fall back to the torch.hub cache @@ -86,6 +93,7 @@ def speech_samples(self) -> int: def reset(self): self._reset() + self._reset_firered_adapter() def effective_silence_limit_chunks(self) -> int: return self._get_effective_silence_limit() @@ -117,12 +125,34 @@ def _update_adaptive_limit(self): self._silence_limit = new_limit def update_settings(self, settings: dict): + old_mode = self.mode + old_firered_key = self._firered_config_key() if "vad_mode" in settings: - self.mode = settings["vad_mode"] + mode = settings["vad_mode"] + if mode in ("silero", "firered", "energy", "disabled"): + self.mode = mode + else: + log.warning(f"Unknown VAD mode '{mode}', keeping {self.mode}") if "vad_threshold" in settings: self.threshold = settings["vad_threshold"] if "energy_threshold" in settings: self.energy_threshold = settings["energy_threshold"] + if "firered_vad_model" in settings: + self._firered_model = str(settings.get("firered_vad_model") or "") + if "firered_vad_use_gpu" in settings: + self._firered_use_gpu = bool(settings.get("firered_vad_use_gpu")) + if "firered_vad_smooth_window_size" in settings: + try: + self._firered_smooth_window_size = max( + 1, int(settings.get("firered_vad_smooth_window_size") or 5) + ) + except (TypeError, ValueError): + self._firered_smooth_window_size = 5 + if "firered_vad_frame_aggregation" in settings: + agg = str(settings.get("firered_vad_frame_aggregation") or "max").lower() + self._firered_frame_aggregation = ( + agg if agg in ("max", "latest", "mean") else "max" + ) if "min_speech_duration" in settings: self.min_speech_samples = int( settings["min_speech_duration"] * self.sample_rate @@ -137,6 +167,10 @@ def update_settings(self, settings: dict): self._fixed_silence_dur = settings["silence_duration"] if self._silence_mode == "fixed": self._silence_limit = self._seconds_to_chunks(self._fixed_silence_dur) + if old_firered_key != self._firered_config_key(): + self._unload_firered_adapter() + elif old_mode != self.mode: + self._reset_firered_adapter() log.info( f"VAD settings updated: mode={self.mode}, threshold={self.threshold}, " f"silence={self._silence_mode} " @@ -156,14 +190,98 @@ def _energy_confidence(self, audio_chunk: np.ndarray) -> float: rms = float(np.sqrt(np.mean(audio_chunk**2))) return min(1.0, rms / (self.energy_threshold * 2)) + def _firered_config_key(self, resolved_model: str | None = None) -> tuple: + return ( + resolved_model or self._firered_model, + self._firered_use_gpu, + self._firered_smooth_window_size, + self._firered_frame_aggregation, + ) + + def _reset_firered_adapter(self): + if self._firered is not None: + self._firered.reset() + + def _unload_firered_adapter(self): + if self._firered is not None: + self._firered.unload() + self._firered = None + self._firered_failed_key = None + self._firered_missing_warned_for = None + + def _fallback_silero_confidence(self, audio_chunk: np.ndarray) -> float: + return self._silero_confidence(audio_chunk) + + def _firered_confidence(self, audio_chunk: np.ndarray) -> float: + if not self._firered_model: + if self._firered_missing_warned_for != "": + log.warning( + "FireRedVAD selected but no Stream-VAD model is configured; " + "using Silero VAD confidence" + ) + self._firered_missing_warned_for = "" + return self._fallback_silero_confidence(audio_chunk) + + try: + from model_manager import get_firered_vad_model_path + + model_path = get_firered_vad_model_path(self._firered_model) + except Exception as exc: + log.warning(f"FireRedVAD model path resolution failed: {exc}") + model_path = None + + if not model_path: + if self._firered_missing_warned_for != self._firered_model: + log.warning( + "FireRedVAD Stream-VAD model is unavailable: %s; " + "using Silero VAD confidence", + self._firered_model, + ) + self._firered_missing_warned_for = self._firered_model + return self._fallback_silero_confidence(audio_chunk) + + config_key = self._firered_config_key(model_path) + if self._firered is None: + if self._firered_failed_key == config_key: + return self._fallback_silero_confidence(audio_chunk) + try: + from vad_firered import FireRedVadAdapter + + self._firered = FireRedVadAdapter( + model_dir=model_path, + threshold=self.threshold, + smooth_window_size=self._firered_smooth_window_size, + use_gpu=self._firered_use_gpu, + frame_aggregation=self._firered_frame_aggregation, + ) + self._firered_failed_key = None + except Exception as exc: + log.warning(f"FireRedVAD unavailable: {exc}") + self._firered_failed_key = config_key + self._firered = None + return self._fallback_silero_confidence(audio_chunk) + + try: + return self._firered.confidence_for_chunk(audio_chunk) + except Exception as exc: + log.warning(f"FireRedVAD confidence failed, falling back to Silero: {exc}") + self._unload_firered_adapter() + self._firered_failed_key = config_key + return self._fallback_silero_confidence(audio_chunk) + def _get_confidence(self, audio_chunk: np.ndarray) -> float: if self.mode == "silero": return self._silero_confidence(audio_chunk) + elif self.mode == "firered": + return self._firered_confidence(audio_chunk) elif self.mode == "energy": return self._energy_confidence(audio_chunk) else: # disabled return 1.0 + def _effective_threshold(self) -> float: + return self.threshold if self.mode in ("silero", "firered") else 0.5 + def _get_effective_silence_limit(self) -> int: """Progressive silence: accept shorter pauses as split points when buffer is long.""" buf_seconds = self._speech_samples / self.sample_rate @@ -179,7 +297,7 @@ def process_chunk(self, audio_chunk: np.ndarray): confidence = self._get_confidence(audio_chunk) self.last_confidence = confidence - effective_threshold = self.threshold if self.mode == "silero" else 0.5 + effective_threshold = self._effective_threshold() eff_silence_limit = self._get_effective_silence_limit() if confidence >= effective_threshold: @@ -278,7 +396,7 @@ def _find_best_split_index(self) -> int: avg_conf = sum(smoothed[search_start:]) / max(1, n - search_start) dip_ratio = min_val / max(avg_conf, 1e-6) - effective_threshold = self.threshold if self.mode == "silero" else 0.5 + effective_threshold = self._effective_threshold() if min_val < effective_threshold or dip_ratio < 0.8: log.debug( f"Split point at chunk {min_idx}/{n}: " @@ -341,7 +459,7 @@ def _flush_segment(self): return None # Speech density check: discard segments where most chunks are below threshold if len(self._confidence_history) >= 4: - effective_threshold = self.threshold if self.mode == "silero" else 0.5 + effective_threshold = self._effective_threshold() voiced = sum( 1 for c in self._confidence_history if c >= effective_threshold ) From ef218dae55e2fe29816c8f55f15b91d9d56b768b Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sun, 21 Jun 2026 04:12:56 +0800 Subject: [PATCH 11/13] =?UTF-8?q?feat(asr):=20=E6=94=AF=E6=8C=81=20parakee?= =?UTF-8?q?t.cpp=20ASR=20=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 worker-only parakeet.cpp C API adapter,支持本地 GGUF 模型、runtime DLL 加载、语言设置和 text/JSON 转写路径。 - 接入 ASR worker 与 ASRService 切换流程,缺模型或 runtime 时返回 invalid plan,切换失败沿用现有 worker restore 机制。 - 增加 parakeet.cpp 模型与 runtime 扫描,避免与 CrispASR 的 GGUF 模型列表互相污染,并支持 models 下 parakeet 命名 runtime 目录。 - 扩展设置面板、配置默认值和 i18n,支持选择模型、runtime、backend、decoder 与词级时间戳选项。 - 更新 install.ps1 可选安装参数,以及 README/README_zh/CLAUDE 中的使用说明和维护约束。 --- CLAUDE.md | 5 + README.md | 24 ++- README_zh.md | 24 ++- asr_parakeet_cpp.py | 306 +++++++++++++++++++++++++++++++++++++ asr_service.py | 150 ++++++++++++++++++ asr_worker.py | 12 ++ config.yaml | 12 +- control_panel.py | 359 ++++++++++++++++++++++++++++++++++++++++++-- i18n/en.yaml | 21 +++ i18n/zh.yaml | 21 +++ install.ps1 | 98 +++++++++++- main.py | 15 ++ model_manager.py | 318 ++++++++++++++++++++++++++++++++++++++- settings_store.py | 15 ++ 14 files changed, 1363 insertions(+), 17 deletions(-) create mode 100644 asr_parakeet_cpp.py diff --git a/CLAUDE.md b/CLAUDE.md index a8ba486..80c6c4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,7 @@ main.py (LiveTranslateApp) |-- asr_anime_whisper.py Anime-Whisper backend (litagin/anime-whisper, ja anime/galgame) |-- asr_crispasr.py CrispASR ggml runtime backend (GGUF/bin single-file models) |-- asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer backend (local ONNX model dirs) + |-- asr_parakeet_cpp.py parakeet.cpp C API backend (local GGUF model + native runtime) |-- translator.py OpenAI-compatible API client, streaming, JSON schema, context history |-- subtitle_overlay.py PyQt6 transparent overlay (2-row header: controls + model/lang combos) |-- subtitle_window.py Standalone subtitle window for OBS capture (outlined text, animations) @@ -177,6 +178,10 @@ Continuous speech is processed incrementally to reduce latency (enabled by `incr - ASR engine lifecycle: the GUI process never instantiates `ASREngine`, `FunASREngine`, or `AnimeWhisperEngine` directly. It owns an `ASRClient`; each worker process owns one concrete backend/model. Engine/model/device changes shut down the current worker first, then start a new worker. On target load failure, the saved previous worker config is used to restore ASR. - CrispASR is treated as a ggml C++ runtime hub with GGUF/bin single-file weights. The GUI process never imports `crispasr`; `asr_crispasr.py` imports the Python binding only inside the ASR worker process and adapts results to the common ASR dict. - sherpa-onnx is treated as a local ONNX ASR runtime. The GUI process never imports `sherpa_onnx`; `asr_sherpa_onnx.py` imports it lazily inside the ASR worker after any CUDA device mapping is finalized. Offline families use `OfflineRecognizer`; `online_transducer` uses `OnlineRecognizer` as a VAD segment wrapper, not true partial streaming. +- parakeet.cpp is treated as a worker-only native C API backend. The GUI process never loads the parakeet DLL; `asr_parakeet_cpp.py` uses `ctypes` only inside the ASR worker, loads one local `.gguf` once, and returns the common ASR dict. +- parakeet.cpp model scanning only accepts known official GGUF filename prefixes or explicit sidecar metadata (`family=parakeet_cpp` / `architecture=parakeet`). CrispASR scanning excludes recognized parakeet GGUF files to avoid `.gguf` list pollution. +- parakeet.cpp C API returned strings must be released with `parakeet_capi_free_string()`, and loaded contexts must be released with `parakeet_capi_free()`. +- parakeet.cpp runtime DLL paths are added only inside the worker with `os.add_dll_directory()`. CUDA runtime may require the matching `cudart-parakeet-bin-win-cuda-x64.zip` DLLs beside the parakeet runtime. - CrispASR packaging has two parts that must stay aligned: `pyproject.toml` installs the pure-Python binding from the CrispASR release tag via `uv sync`, while `install.ps1` and the portable `bootstrap.ps1` download the matching prebuilt Windows `libcrispasr` DLL runtime from GitHub Releases. Do not replace this with plain `crispasr` from PyPI; that package name may not exist in the configured registry and the binding alone does not include `crispasr.dll`. - CrispASR native runtime install prefers `libcrispasr-windows-x86_64-cuda.tar.gz` on NVIDIA systems and falls back to `libcrispasr-windows-x86_64.tar.gz`. DLLs are copied next to the installed `crispasr` package so the worker can load them without a global PATH edit. - Whisper (ctranslate2) only accepts `device="cuda"` not `"cuda:0"`; device index passed via `device_index` param. Parsed from combo text like `"cuda:0 (RTX 4090)"` in `_switch_asr_engine` diff --git a/README.md b/README.md index 078aa21..3a8191e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Works with any system audio — videos, livestreams, voice chat. No player modif ## Features - **Real-time pipeline**: System audio → VAD → ASR → LLM translation → overlay -- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper, CrispASR, sherpa-onnx, Remote Whisper +- **Multiple ASR engines**: faster-whisper, SenseVoice, FunASR Nano, Anime-Whisper, CrispASR, sherpa-onnx, parakeet.cpp, Remote Whisper - **Remote ASR**: offload speech recognition to a GPU machine over HTTP — see [REMOTE_ASR.md](REMOTE_ASR.md) - **Any OpenAI-compatible API**: DeepSeek, Grok, Qwen, GPT, Ollama, vLLM, etc. - **Streaming translation display**: Real-time character-by-character translation output @@ -128,6 +128,26 @@ powershell -ExecutionPolicy Bypass -File install.ps1 -SherpaOnnxRuntime cuda12 Download sherpa-onnx ASR model archives from the official sherpa-onnx releases, extract them anywhere under `models/`, then open Settings → VAD/ASR, choose `sherpa-onnx (ONNX)`, click Refresh, and select the local model directory. Online transducer scans accept `encoder.onnx`/`decoder.onnx`/`joiner.onnx` and int8 variants such as `encoder.int8.onnx`/`decoder.int8.onnx`/`joiner.int8.onnx`. PR #3671 Nemotron packages are published with names like `sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-560ms-int8-2026-06-11`; unofficial snapshots must still have ONNX files accepted by the installed sherpa-onnx/ONNX Runtime version. The `onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4` layout is not treated as a sherpa-onnx model in this path. +## parakeet.cpp Models + +parakeet.cpp is optional and is not downloaded during first launch. It uses local GGUF models plus a native parakeet.cpp runtime. The first integration uses LiveTranslate's existing VAD segments and ASR worker process; parakeet.cpp streaming EOU is not connected yet. + +Optional installer commands: + +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 -InstallParakeetCpp -ParakeetCppBackend cpu +powershell -ExecutionPolicy Bypass -File install.ps1 -DownloadParakeetCppModel -ParakeetCppModel tdt_ctc-110m-q4_k +``` + +Manual layout: + +```text +models/parakeet.cpp/runtime/v0.3.2// +models/parakeet.cpp/models/tdt_ctc-110m-q4_k.gguf +``` + +Then open Settings → VAD/ASR, choose `parakeet.cpp (GGUF)`, click Refresh for both model and runtime, and select them. CUDA runtime may require the matching `cudart-parakeet-bin-win-cuda-x64.zip` asset next to the runtime DLLs. + ## Translation API Settings → Translation tab: @@ -160,6 +180,7 @@ main.py Entry point & pipeline ├── asr_anime_whisper.py Anime-Whisper backend (ja anime/galgame) ├── asr_crispasr.py CrispASR ggml runtime backend ├── asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer backend +├── asr_parakeet_cpp.py parakeet.cpp C API GGUF backend ├── asr_remote.py Remote Whisper client (→ asr_server.py, see REMOTE_ASR.md) ├── translator.py OpenAI-compatible client (streaming, JSON schema, context) ├── model_manager.py Model download & cache @@ -176,6 +197,7 @@ main.py Entry point & pipeline - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — Japanese anime/galgame ASR - CrispASR — ggml C++ ASR runtime hub with GGUF/bin single-file models, used through its Python binding in the ASR worker - [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — ONNX ASR runtime used through `OfflineRecognizer` and segment-wrapped `OnlineRecognizer` +- [parakeet.cpp](https://github.com/mudler/parakeet.cpp) — NVIDIA NeMo Parakeet GGUF inference through the C API in the ASR worker - [Silero VAD](https://github.com/snakers4/silero-vad) — Voice activity detection - [FireRedVAD](https://github.com/FireRedTeam/FireRedVAD) — Optional streaming VAD confidence backend diff --git a/README_zh.md b/README_zh.md index 3edcc77..7e78422 100644 --- a/README_zh.md +++ b/README_zh.md @@ -21,7 +21,7 @@ Windows 实时音频翻译工具。捕获系统音频(WASAPI loopback)和可 ## 功能特性 - **实时翻译管线**:系统音频 → VAD → ASR → LLM 翻译 → 字幕显示 -- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper、CrispASR、sherpa-onnx、Remote Whisper +- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-Whisper、CrispASR、sherpa-onnx、parakeet.cpp、Remote Whisper - **远程 ASR**:通过 HTTP 把语音识别放到 GPU 机器上跑 —— 见 [REMOTE_ASR.md](REMOTE_ASR.md) - **兼容任意 OpenAI 格式 API**:DeepSeek、Grok、Qwen、GPT、Ollama、vLLM 等 - **流式翻译显示**:翻译结果逐字实时显示 @@ -128,6 +128,26 @@ powershell -ExecutionPolicy Bypass -File install.ps1 -SherpaOnnxRuntime cuda12 从 sherpa-onnx 官方 releases 下载 ASR 模型压缩包,解压到 `models/` 下任意子目录;然后在 设置 → VAD/ASR 中选择 `sherpa-onnx (ONNX)`,点击刷新并选择本地模型目录。Online transducer 扫描支持 `encoder.onnx`/`decoder.onnx`/`joiner.onnx`,也支持 `encoder.int8.onnx`/`decoder.int8.onnx`/`joiner.int8.onnx`。PR #3671 对应的 Nemotron 包名类似 `sherpa-onnx-nemotron-3.5-asr-streaming-0.6b-560ms-int8-2026-06-11`;非官方 snapshot 的 ONNX 文件仍必须能被当前安装的 sherpa-onnx/ONNX Runtime 接受。`onnx-community/nemotron-3.5-asr-streaming-0.6b-onnx-int4` 这类结构不属于此 sherpa-onnx 路线。 +## parakeet.cpp 模型 + +parakeet.cpp 是可选 ASR 后端,首次启动不会强制下载。它使用本地 GGUF 模型和 parakeet.cpp native runtime。当前第一阶段仍使用 LiveTranslate 现有 VAD 切段和 ASR worker,暂不接入 parakeet.cpp streaming EOU。 + +可选安装命令: + +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 -InstallParakeetCpp -ParakeetCppBackend cpu +powershell -ExecutionPolicy Bypass -File install.ps1 -DownloadParakeetCppModel -ParakeetCppModel tdt_ctc-110m-q4_k +``` + +手动放置路径: + +```text +models/parakeet.cpp/runtime/v0.3.2// +models/parakeet.cpp/models/tdt_ctc-110m-q4_k.gguf +``` + +然后在 设置 → VAD/ASR 中选择 `parakeet.cpp (GGUF)`,分别刷新并选择本地 GGUF 模型和 runtime。CUDA runtime 可能需要把匹配的 `cudart-parakeet-bin-win-cuda-x64.zip` 一并解压到 runtime DLL 附近。 + ## 配置翻译 API 设置 → 翻译标签页: @@ -160,6 +180,7 @@ main.py 主入口,管线编排 ├── asr_anime_whisper.py Anime-Whisper 后端 (日语动画/Galgame) ├── asr_crispasr.py CrispASR ggml runtime 后端 ├── asr_sherpa_onnx.py sherpa-onnx OfflineRecognizer/OnlineRecognizer 后端 +├── asr_parakeet_cpp.py parakeet.cpp C API GGUF 后端 ├── asr_remote.py 远程 Whisper 客户端 (→ asr_server.py, 见 REMOTE_ASR.md) ├── translator.py OpenAI 兼容翻译客户端 (流式/JSON/上下文) ├── model_manager.py 模型下载与缓存管理 @@ -176,6 +197,7 @@ main.py 主入口,管线编排 - [Anime-Whisper](https://huggingface.co/litagin/anime-whisper) — 日语动画/Galgame 专用 ASR - CrispASR — ggml C++ ASR runtime hub,使用 GGUF/bin 单文件模型,LiveTranslate 通过 ASR worker 内的 Python binding 调用 - [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) — 通过 `OfflineRecognizer` 和 segment-wrapper `OnlineRecognizer` 调用的 ONNX ASR runtime +- [parakeet.cpp](https://github.com/mudler/parakeet.cpp) — 通过 ASR worker 内 C API 调用的 NVIDIA NeMo Parakeet GGUF 推理后端 - [Silero VAD](https://github.com/snakers4/silero-vad) — 语音活动检测 - [FireRedVAD](https://github.com/FireRedTeam/FireRedVAD) — 可选 streaming VAD confidence 后端 diff --git a/asr_parakeet_cpp.py b/asr_parakeet_cpp.py new file mode 100644 index 0000000..aa3a0d1 --- /dev/null +++ b/asr_parakeet_cpp.py @@ -0,0 +1,306 @@ +import ctypes +import json +import logging +import os +from pathlib import Path + +import numpy as np + +from translator import LANGUAGE_DISPLAY + +log = logging.getLogger("LiveTranslate.ParakeetCpp") + +LANGUAGE_NAMES = {**LANGUAGE_DISPLAY, "auto": "auto"} +_LIBRARY_NAMES = ( + "parakeet.dll", + "libparakeet.dll", + "parakeet_capi.dll", + "libparakeet_capi.dll", +) +_DECODER_IDS = {"auto": 0, "ctc": 1, "tdt": 2, "rnnt": 2} + + +def _find_library(runtime_dir: Path) -> Path | None: + for name in _LIBRARY_NAMES: + candidate = runtime_dir / name + if candidate.is_file(): + return candidate + for name in _LIBRARY_NAMES: + matches = list(runtime_dir.rglob(name)) + if matches: + return matches[0] + return None + + +def _parse_device_index(device: str) -> int: + device = str(device or "cpu").split(" (", 1)[0].strip() + if device.startswith("cuda:"): + try: + return int(device.split(":", 1)[1]) + except ValueError: + return 0 + return 0 + + +class ParakeetCppEngine: + """parakeet.cpp C API adapter for VAD-segmented 16 kHz mono float32 audio.""" + + def __init__( + self, + model_path: str, + runtime_dir: str, + backend: str = "auto", + decoder: str = "auto", + device: str = "cpu", + language: str = "auto", + word_timestamps: bool = True, + ): + self.model_path = str(Path(model_path).resolve()) + self.runtime_dir = str(Path(runtime_dir).resolve()) + self.backend = str(backend or "auto").lower() + self.decoder = str(decoder or "auto").lower() + self.device = str(device or "cpu") + self.language = str(language or "auto") + self.word_timestamps = bool(word_timestamps) + self._dll_handles = [] + self._lib = None + self._ctx = None + self._abi_version = 0 + self._json_available = False + + if self.backend not in ("auto", "cpu", "cuda", "vulkan"): + raise ValueError(f"Unsupported parakeet.cpp backend: {self.backend}") + if self.decoder not in _DECODER_IDS: + raise ValueError(f"Unsupported parakeet.cpp decoder: {self.decoder}") + if not Path(self.model_path).is_file(): + raise FileNotFoundError(f"parakeet.cpp model not found: {self.model_path}") + runtime_path = Path(self.runtime_dir) + if not runtime_path.is_dir(): + raise FileNotFoundError(f"parakeet.cpp runtime dir not found: {runtime_path}") + + library_path = _find_library(runtime_path) + if not library_path: + raise FileNotFoundError( + f"parakeet.cpp shared library not found in runtime dir: {runtime_path}" + ) + + self._configure_backend_env() + self._add_dll_directories(runtime_path, library_path.parent) + self._lib = ctypes.CDLL(str(library_path)) + self._bind_api() + if self._abi_version < 3: + raise RuntimeError( + f"parakeet.cpp C API ABI {self._abi_version} is too old; " + "ABI >= 3 is required" + ) + self._ctx = self._lib.parakeet_capi_load(self.model_path.encode("utf-8")) + if not self._ctx: + raise RuntimeError(f"parakeet.cpp failed to load model: {self._last_error()}") + + log.info( + "parakeet.cpp loaded: " + f"model={self.model_path}, runtime={self.runtime_dir}, " + f"backend={self.backend}, decoder={self.decoder}, abi={self._abi_version}" + ) + + def _configure_backend_env(self): + backend = self.backend + device_index = _parse_device_index(self.device) + if backend == "auto": + backend = "cuda" if self.device.split(" (", 1)[0].startswith("cuda") else "cpu" + if backend == "cpu": + os.environ["PARAKEET_DEVICE"] = "cpu" + elif backend == "cuda": + os.environ["PARAKEET_DEVICE"] = f"CUDA{device_index}" + elif backend == "vulkan": + os.environ["PARAKEET_DEVICE"] = f"Vulkan{device_index}" + + def _add_dll_directories(self, runtime_path: Path, library_dir: Path): + if os.name != "nt" or not hasattr(os, "add_dll_directory"): + return + dirs = {runtime_path, library_dir} + for child in runtime_path.rglob("*"): + if child.is_dir() and any(child.glob("*.dll")): + dirs.add(child) + for directory in dirs: + try: + self._dll_handles.append(os.add_dll_directory(str(directory))) + except OSError: + log.warning(f"Failed to add parakeet.cpp DLL directory: {directory}") + + def _bind_api(self): + lib = self._lib + lib.parakeet_capi_abi_version.argtypes = [] + lib.parakeet_capi_abi_version.restype = ctypes.c_int + self._abi_version = int(lib.parakeet_capi_abi_version()) + + lib.parakeet_capi_load.argtypes = [ctypes.c_char_p] + lib.parakeet_capi_load.restype = ctypes.c_void_p + lib.parakeet_capi_free.argtypes = [ctypes.c_void_p] + lib.parakeet_capi_free.restype = None + lib.parakeet_capi_free_string.argtypes = [ctypes.c_void_p] + lib.parakeet_capi_free_string.restype = None + + lib.parakeet_capi_transcribe_pcm_lang.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.c_float), + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_int, + ctypes.c_char_p, + ] + lib.parakeet_capi_transcribe_pcm_lang.restype = ctypes.c_void_p + + lib.parakeet_capi_last_error.argtypes = [ctypes.c_void_p] + lib.parakeet_capi_last_error.restype = ctypes.c_char_p + + batch_json = getattr(lib, "parakeet_capi_transcribe_pcm_batch_json_lang", None) + if batch_json is not None: + batch_json.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), + ctypes.POINTER(ctypes.c_size_t), + ctypes.c_size_t, + ctypes.c_int, + ctypes.c_int, + ctypes.c_char_p, + ] + batch_json.restype = ctypes.c_void_p + self._json_available = True + + def set_language(self, language: str): + self.language = str(language or "auto") + + def unload(self): + ctx = self._ctx + self._ctx = None + if ctx and self._lib: + try: + self._lib.parakeet_capi_free(ctx) + except Exception: + log.warning("parakeet.cpp context free failed", exc_info=True) + for handle in self._dll_handles: + try: + handle.close() + except Exception: + pass + self._dll_handles.clear() + + def transcribe( + self, audio: np.ndarray, word_timestamps: bool = False + ) -> dict | None: + if audio.size == 0: + return None + if audio.dtype != np.float32 or not audio.flags["C_CONTIGUOUS"]: + audio = np.ascontiguousarray(audio, dtype=np.float32) + want_words = bool(word_timestamps and self.word_timestamps) + if want_words and self._json_available: + try: + return self._transcribe_json(audio) + except Exception as exc: + log.warning( + f"parakeet.cpp JSON timestamps failed, falling back to text: {exc}" + ) + text = self._transcribe_text(audio) + if not text: + return None + language = self.language or "auto" + if language == "auto": + language = "en" + return { + "text": text, + "language": language, + "language_name": LANGUAGE_NAMES.get(language, language), + } + + def _target_lang_bytes(self) -> bytes: + language = self.language or "auto" + return language.encode("utf-8") + + def _decoder_id(self) -> int: + return _DECODER_IDS.get(self.decoder, 0) + + def _transcribe_text(self, audio: np.ndarray) -> str: + ptr = audio.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) + result_ptr = self._lib.parakeet_capi_transcribe_pcm_lang( + self._ctx, + ptr, + ctypes.c_size_t(audio.size), + 16000, + self._decoder_id(), + self._target_lang_bytes(), + ) + if not result_ptr: + raise RuntimeError(f"parakeet.cpp transcription failed: {self._last_error()}") + try: + return ctypes.string_at(result_ptr).decode("utf-8", errors="replace").strip() + finally: + self._lib.parakeet_capi_free_string(result_ptr) + + def _transcribe_json(self, audio: np.ndarray) -> dict | None: + sample_ptr = audio.ctypes.data_as(ctypes.POINTER(ctypes.c_float)) + batch_type = ctypes.POINTER(ctypes.c_float) * 1 + batch = batch_type(sample_ptr) + lengths_type = ctypes.c_size_t * 1 + lengths = lengths_type(audio.size) + fn = self._lib.parakeet_capi_transcribe_pcm_batch_json_lang + result_ptr = fn( + self._ctx, + batch, + lengths, + 1, + 16000, + self._decoder_id(), + self._target_lang_bytes(), + ) + if not result_ptr: + raise RuntimeError(f"parakeet.cpp JSON transcription failed: {self._last_error()}") + try: + raw = ctypes.string_at(result_ptr).decode("utf-8", errors="replace") + finally: + self._lib.parakeet_capi_free_string(result_ptr) + data = json.loads(raw) + if isinstance(data, list): + data = data[0] if data else {} + if not isinstance(data, dict): + return None + text = str(data.get("text") or "").strip() + if not text: + return None + language = self.language or "auto" + if language == "auto": + language = "en" + result = { + "text": text, + "language": language, + "language_name": LANGUAGE_NAMES.get(language, language), + } + words = [] + for item in data.get("words") or []: + if not isinstance(item, dict): + continue + word = item.get("word", item.get("w", "")) + if not word: + continue + words.append( + { + "word": str(word), + "start": float(item.get("start", 0.0) or 0.0), + "end": float(item.get("end", 0.0) or 0.0), + "probability": float(item.get("probability", item.get("conf", 0.0)) or 0.0), + } + ) + if words: + result["words"] = words + return result + + def _last_error(self) -> str: + if not self._lib: + return "native library not loaded" + try: + value = self._lib.parakeet_capi_last_error(self._ctx) + except Exception: + return "unknown native error" + if not value: + return "unknown native error" + return value.decode("utf-8", errors="replace") diff --git a/asr_service.py b/asr_service.py index 300333a..e57ca5c 100644 --- a/asr_service.py +++ b/asr_service.py @@ -9,7 +9,9 @@ ASR_DISPLAY_NAMES, DEFAULT_FUNASR_MODEL, MODELS_DIR, + detect_parakeet_cpp_runtime_dir, detect_sherpa_onnx_model_dir, + get_parakeet_cpp_model_path, funasr_display_name, funasr_supports_padding, get_missing_models, @@ -17,9 +19,11 @@ is_asr_cached, local_crispasr_display_name, local_faster_whisper_display_name, + local_parakeet_cpp_display_name, local_sherpa_onnx_display_name, normalize_asr_engine_selection, normalize_funasr_model_key, + resolve_parakeet_cpp_runtime_dir, resolve_custom_crispasr_model, resolve_custom_whisper_model, ) @@ -88,6 +92,12 @@ def __init__( self._sherpa_onnx_model_path = str( config["asr"].get("sherpa_onnx_model", "") or "" ) + self._parakeet_cpp_model_key = str( + config["asr"].get("parakeet_cpp_model", "") or "" + ) + self._parakeet_cpp_runtime_dir = str( + config["asr"].get("parakeet_cpp_runtime_dir", "") or "" + ) self._asr_lock = threading.RLock() @property @@ -216,6 +226,46 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: ) sherpa_onnx_model_path = None sherpa_onnx_model_info = None + parakeet_cpp_model = str( + settings.get( + "parakeet_cpp_model", + self._config["asr"].get( + "parakeet_cpp_model", self._parakeet_cpp_model_key + ), + ) + or "" + ) + parakeet_cpp_runtime_dir_value = str( + settings.get( + "parakeet_cpp_runtime_dir", + self._config["asr"].get( + "parakeet_cpp_runtime_dir", self._parakeet_cpp_runtime_dir + ), + ) + or "" + ) + parakeet_cpp_backend = str( + settings.get( + "parakeet_cpp_backend", + self._config["asr"].get("parakeet_cpp_backend", "auto"), + ) + or "auto" + ).lower() + parakeet_cpp_decoder = str( + settings.get( + "parakeet_cpp_decoder", + self._config["asr"].get("parakeet_cpp_decoder", "auto"), + ) + or "auto" + ).lower() + parakeet_cpp_word_timestamps = bool( + settings.get( + "parakeet_cpp_word_timestamps", + self._config["asr"].get("parakeet_cpp_word_timestamps", True), + ) + ) + parakeet_cpp_model_path = None + parakeet_cpp_runtime_dir = None remote_asr_url = str( settings.get( "remote_asr_url", @@ -286,6 +336,65 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: f"{sherpa_onnx_model_path}; keeping current ASR worker", ) cache_model_key = sherpa_onnx_model_path + elif engine_type == "parakeet-cpp": + if not parakeet_cpp_model: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + "parakeet.cpp model is not selected; keeping current ASR worker", + ) + parakeet_cpp_model_path = get_parakeet_cpp_model_path(parakeet_cpp_model) + if not parakeet_cpp_model_path: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"parakeet.cpp model file is unavailable or unrecognized: " + f"{parakeet_cpp_model}; keeping current ASR worker", + ) + if parakeet_cpp_backend not in ("auto", "cpu", "cuda", "vulkan"): + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"Unsupported parakeet.cpp backend: {parakeet_cpp_backend}", + ) + if parakeet_cpp_decoder not in ("auto", "ctc", "tdt", "rnnt"): + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"Unsupported parakeet.cpp decoder: {parakeet_cpp_decoder}", + ) + parakeet_cpp_runtime_dir = resolve_parakeet_cpp_runtime_dir( + parakeet_cpp_runtime_dir_value, + "auto" if parakeet_cpp_backend == "auto" else parakeet_cpp_backend, + ) + if not parakeet_cpp_runtime_dir: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"parakeet.cpp runtime dir is unavailable or unrecognized: " + f"{parakeet_cpp_runtime_dir_value}; keeping current ASR worker", + ) + runtime_info = detect_parakeet_cpp_runtime_dir(parakeet_cpp_runtime_dir) + if not runtime_info: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + f"parakeet.cpp runtime dir is unavailable or unrecognized: " + f"{parakeet_cpp_runtime_dir}; keeping current ASR worker", + ) + cache_model_key = parakeet_cpp_model_path elif engine_type == "remote-whisper": cache_model_key = remote_asr_url @@ -313,6 +422,14 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: sherpa_onnx_left_padding_seconds, sherpa_onnx_tail_padding_seconds, ) + elif engine_type == "parakeet-cpp": + signature_model = ( + parakeet_cpp_model_path, + parakeet_cpp_runtime_dir, + parakeet_cpp_backend, + parakeet_cpp_decoder, + parakeet_cpp_word_timestamps, + ) elif engine_type == "remote-whisper": signature_model = remote_asr_url else: @@ -357,6 +474,7 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: funasr_model, crispasr_model, sherpa_onnx_model_path, + parakeet_cpp_model_path, ) worker_config = { "engine_type": engine_type, @@ -408,6 +526,17 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: "sherpa_onnx_tail_padding_seconds": sherpa_onnx_tail_padding_seconds, } ) + elif engine_type == "parakeet-cpp": + worker_config.update( + { + "parakeet_cpp_model": parakeet_cpp_model, + "parakeet_cpp_model_path": parakeet_cpp_model_path, + "parakeet_cpp_runtime_dir": parakeet_cpp_runtime_dir, + "parakeet_cpp_backend": parakeet_cpp_backend, + "parakeet_cpp_decoder": parakeet_cpp_decoder, + "parakeet_cpp_word_timestamps": parakeet_cpp_word_timestamps, + } + ) target_state = { "type": engine_type, "signature": signature, @@ -424,6 +553,12 @@ def prepare_switch(self, engine_type: str, settings: dict) -> ASRSwitchPlan: "sherpa_onnx_model_path": sherpa_onnx_model_path if engine_type == "sherpa-onnx" else self._sherpa_onnx_model_path, + "parakeet_cpp_model_key": parakeet_cpp_model + if engine_type == "parakeet-cpp" + else self._parakeet_cpp_model_key, + "parakeet_cpp_runtime_dir": parakeet_cpp_runtime_dir + if engine_type == "parakeet-cpp" + else self._parakeet_cpp_runtime_dir, "config": worker_config, "display_name": display_name, "device_label": remote_asr_url @@ -464,6 +599,8 @@ def switch_worker(self, plan: ASRSwitchPlan) -> ASRSwitchResult: "whisper_model_size": self._whisper_model_size, "crispasr_model_key": self._crispasr_model_key, "sherpa_onnx_model_path": self._sherpa_onnx_model_path, + "parakeet_cpp_model_key": self._parakeet_cpp_model_key, + "parakeet_cpp_runtime_dir": self._parakeet_cpp_runtime_dir, "config": old_config, "display_name": (old_config or {}).get("display_name"), } @@ -671,6 +808,12 @@ def _activate_asr(self, client: ASRClient, state: dict): self._sherpa_onnx_model_path = state.get( "sherpa_onnx_model_path", self._sherpa_onnx_model_path ) + self._parakeet_cpp_model_key = state.get( + "parakeet_cpp_model_key", self._parakeet_cpp_model_key + ) + self._parakeet_cpp_runtime_dir = state.get( + "parakeet_cpp_runtime_dir", self._parakeet_cpp_runtime_dir + ) self._asr_ready = True self._asr_error_count = 0 @@ -682,6 +825,7 @@ def _display_name( funasr_model: str, crispasr_model: str, sherpa_onnx_model_path: str | None = None, + parakeet_cpp_model_path: str | None = None, ) -> str: display_name = ASR_DISPLAY_NAMES.get(engine_type, engine_type) if engine_type == "whisper": @@ -707,4 +851,10 @@ def _display_name( or Path(sherpa_onnx_model_path).name ) display_name = f"sherpa-onnx {display_model}" + elif engine_type == "parakeet-cpp" and parakeet_cpp_model_path: + display_model = ( + local_parakeet_cpp_display_name(parakeet_cpp_model_path) + or Path(parakeet_cpp_model_path).name + ) + display_name = f"parakeet.cpp {display_model}" return display_name diff --git a/asr_worker.py b/asr_worker.py index 72ef318..e9283f9 100644 --- a/asr_worker.py +++ b/asr_worker.py @@ -177,6 +177,18 @@ def _load_engine(config: dict): engine = SherpaOnnxEngine(provider="cpu", **kwargs) else: raise + elif engine_type == "parakeet-cpp": + from asr_parakeet_cpp import ParakeetCppEngine + + engine = ParakeetCppEngine( + model_path=config["parakeet_cpp_model_path"], + runtime_dir=config["parakeet_cpp_runtime_dir"], + backend=config.get("parakeet_cpp_backend", "auto"), + decoder=config.get("parakeet_cpp_decoder", "auto"), + device=device, + language=language, + word_timestamps=config.get("parakeet_cpp_word_timestamps", True), + ) else: from asr_engine import ASREngine diff --git a/config.yaml b/config.yaml index 7e350a1..3be95c3 100644 --- a/config.yaml +++ b/config.yaml @@ -9,7 +9,7 @@ audio: chunk_duration: 0.032 asr: - # ASR engine: whisper, funasr, anime-whisper, remote-whisper, crispasr, sherpa-onnx + # ASR engine: whisper, funasr, anime-whisper, remote-whisper, crispasr, sherpa-onnx, parakeet-cpp asr_engine: "funasr" # Model size: tiny, base, small, medium, large-v3 model_size: "medium" @@ -24,6 +24,16 @@ asr: crispasr_unified_memory: true # sherpa-onnx local model directory. Empty means user has not selected a model yet. sherpa_onnx_model: "" + # parakeet.cpp local GGUF model path. Empty means user has not selected a model yet. + parakeet_cpp_model: "" + # Runtime directory containing parakeet shared library and dependency DLLs. + parakeet_cpp_runtime_dir: "" + # parakeet.cpp backend: auto, cpu, cuda, vulkan. + parakeet_cpp_backend: "auto" + # parakeet.cpp decoder: auto, ctc, tdt. + parakeet_cpp_decoder: "auto" + # Use C API JSON path when word timestamps are requested. + parakeet_cpp_word_timestamps: true # Remote Whisper server URL used when asr_engine is remote-whisper. remote_asr_url: "http://127.0.0.1:8765" # sherpa-onnx provider: auto, cpu, cuda. Auto uses CUDA only with a CUDA sherpa-onnx wheel and CUDA ASR device. diff --git a/control_panel.py b/control_panel.py index cd9c0a5..eb5d7cf 100644 --- a/control_panel.py +++ b/control_panel.py @@ -42,12 +42,16 @@ funasr_supports_padding, format_size, get_cache_entries, + list_local_parakeet_cpp_models, + list_local_parakeet_cpp_runtimes, list_local_crispasr_models, list_local_faster_whisper_models, list_local_firered_vad_models, list_local_sherpa_onnx_models, migrate_funasr_settings, normalize_funasr_model_key, + resolve_custom_parakeet_cpp_model, + resolve_parakeet_cpp_runtime_dir, resolve_custom_crispasr_model, resolve_custom_firered_vad_model, resolve_custom_sherpa_onnx_model, @@ -140,6 +144,7 @@ def _create_vad_tab(self): ("Remote Whisper (remote GPU server)", "remote-whisper"), ("CrispASR (ggml)", "crispasr"), ("sherpa-onnx (ONNX)", "sherpa-onnx"), + ("parakeet.cpp (GGUF)", "parakeet-cpp"), ): self._asr_engine.addItem(label, key) engine_idx = self._asr_engine.findData(s.get("asr_engine")) @@ -294,6 +299,45 @@ def _create_vad_tab(self): asr_layout.addWidget(self._sherpa_onnx_decoding_label, 10, 0) asr_layout.addWidget(self._sherpa_onnx_decoding_method, 10, 1) + self._parakeet_cpp_backend_label = QLabel(t("label_parakeet_cpp_backend")) + self._parakeet_cpp_backend = QComboBox() + for label, value in (("Auto", "auto"), ("CPU", "cpu"), ("CUDA", "cuda"), ("Vulkan", "vulkan")): + self._parakeet_cpp_backend.addItem(label, value) + parakeet_backend = str(s.get("parakeet_cpp_backend", "auto")).lower() + parakeet_backend_idx = self._parakeet_cpp_backend.findData(parakeet_backend) + if parakeet_backend_idx >= 0: + self._parakeet_cpp_backend.setCurrentIndex(parakeet_backend_idx) + self._parakeet_cpp_backend.currentIndexChanged.connect( + self._on_parakeet_cpp_setting_changed + ) + asr_layout.addWidget(self._parakeet_cpp_backend_label, 11, 0) + asr_layout.addWidget(self._parakeet_cpp_backend, 11, 1) + + self._parakeet_cpp_decoder_label = QLabel(t("label_parakeet_cpp_decoder")) + self._parakeet_cpp_decoder = QComboBox() + for label, value in (("Auto", "auto"), ("CTC", "ctc"), ("TDT/RNNT", "tdt")): + self._parakeet_cpp_decoder.addItem(label, value) + parakeet_decoder = str(s.get("parakeet_cpp_decoder", "auto")).lower() + parakeet_decoder_idx = self._parakeet_cpp_decoder.findData(parakeet_decoder) + if parakeet_decoder_idx >= 0: + self._parakeet_cpp_decoder.setCurrentIndex(parakeet_decoder_idx) + self._parakeet_cpp_decoder.currentIndexChanged.connect( + self._on_parakeet_cpp_setting_changed + ) + asr_layout.addWidget(self._parakeet_cpp_decoder_label, 12, 0) + asr_layout.addWidget(self._parakeet_cpp_decoder, 12, 1) + + self._parakeet_cpp_word_timestamps = QCheckBox( + t("label_parakeet_cpp_word_timestamps") + ) + self._parakeet_cpp_word_timestamps.setChecked( + bool(s.get("parakeet_cpp_word_timestamps", True)) + ) + self._parakeet_cpp_word_timestamps.toggled.connect( + self._on_parakeet_cpp_setting_changed + ) + asr_layout.addWidget(self._parakeet_cpp_word_timestamps, 13, 1) + self._whisper_pad_label = QLabel(t("label_whisper_padding")) self._whisper_pad_seconds = QDoubleSpinBox() self._whisper_pad_seconds.setRange(0.0, 5.0) @@ -307,8 +351,8 @@ def _create_vad_tab(self): self._whisper_pad_seconds.setSuffix(" s") self._whisper_pad_seconds.setSpecialValueText(t("whisper_padding_off")) self._whisper_pad_seconds.setToolTip(t("whisper_padding_tooltip")) - asr_layout.addWidget(self._whisper_pad_label, 11, 0) - asr_layout.addWidget(self._whisper_pad_seconds, 11, 1) + asr_layout.addWidget(self._whisper_pad_label, 14, 0) + asr_layout.addWidget(self._whisper_pad_seconds, 14, 1) self._whisper_pad_seconds.valueChanged.connect(self._auto_save) self._sensevoice_pad_label = QLabel(t("label_sensevoice_padding")) @@ -324,8 +368,8 @@ def _create_vad_tab(self): self._sensevoice_pad_seconds.setSuffix(" s") self._sensevoice_pad_seconds.setSpecialValueText(t("sensevoice_padding_off")) self._sensevoice_pad_seconds.setToolTip(t("sensevoice_padding_tooltip")) - asr_layout.addWidget(self._sensevoice_pad_label, 12, 0) - asr_layout.addWidget(self._sensevoice_pad_seconds, 12, 1) + asr_layout.addWidget(self._sensevoice_pad_label, 15, 0) + asr_layout.addWidget(self._sensevoice_pad_seconds, 15, 1) self._sensevoice_pad_seconds.valueChanged.connect(self._auto_save) self._audio_device = QComboBox() @@ -347,8 +391,8 @@ def _create_vad_tab(self): self._audio_device.setCurrentIndex(idx) else: self._audio_device.setCurrentIndex(1) # system default - asr_layout.addWidget(QLabel(t("label_audio")), 13, 0) - asr_layout.addWidget(self._audio_device, 13, 1) + asr_layout.addWidget(QLabel(t("label_audio")), 16, 0) + asr_layout.addWidget(self._audio_device, 16, 1) self._audio_device.currentIndexChanged.connect(self._auto_save) self._mic_device = QComboBox() @@ -369,16 +413,16 @@ def _create_vad_tab(self): idx = self._mic_device.findText(saved_mic) if idx >= 0: self._mic_device.setCurrentIndex(idx) - asr_layout.addWidget(QLabel(t("label_mic")), 14, 0) - asr_layout.addWidget(self._mic_device, 14, 1) + asr_layout.addWidget(QLabel(t("label_mic")), 17, 0) + asr_layout.addWidget(self._mic_device, 17, 1) self._mic_device.currentIndexChanged.connect(self._auto_save) self._hub_combo = QComboBox() self._hub_combo.addItems([t("hub_modelscope"), t("hub_huggingface")]) saved_hub = s.get("hub", "ms") self._hub_combo.setCurrentIndex(0 if saved_hub == "ms" else 1) - asr_layout.addWidget(QLabel(t("label_hub")), 15, 0) - asr_layout.addWidget(self._hub_combo, 15, 1) + asr_layout.addWidget(QLabel(t("label_hub")), 18, 0) + asr_layout.addWidget(self._hub_combo, 18, 1) self._hub_combo.currentIndexChanged.connect(self._auto_save) self._ui_lang_combo = QComboBox() @@ -387,8 +431,8 @@ def _create_vad_tab(self): saved_lang = s.get("ui_lang", get_lang()) self._ui_lang_combo.setCurrentIndex(0 if saved_lang == "en" else 1) - asr_layout.addWidget(QLabel(t("label_ui_lang")), 16, 0) - asr_layout.addWidget(self._ui_lang_combo, 16, 1) + asr_layout.addWidget(QLabel(t("label_ui_lang")), 19, 0) + asr_layout.addWidget(self._ui_lang_combo, 19, 1) self._ui_lang_combo.currentIndexChanged.connect(self._on_ui_lang_changed) layout.addWidget(asr_group) @@ -450,6 +494,57 @@ def _create_vad_tab(self): self._sherpa_onnx_group.setVisible( self._selected_asr_engine() == "sherpa-onnx" ) + + self._parakeet_cpp_model_group = QGroupBox(t("group_parakeet_cpp_model")) + parakeet_model_layout = QHBoxLayout(self._parakeet_cpp_model_group) + self._parakeet_cpp_model_combo = QComboBox() + saved_parakeet_model = s.get("parakeet_cpp_model", "") + self._populate_parakeet_cpp_models(saved_parakeet_model) + self._parakeet_cpp_model_combo.currentIndexChanged.connect( + self._on_parakeet_cpp_model_changed + ) + parakeet_model_layout.addWidget(self._parakeet_cpp_model_combo) + self._parakeet_cpp_model_status = QLabel("") + self._parakeet_cpp_model_status.setStyleSheet("color: #888; font-size: 11px;") + parakeet_model_layout.addWidget(self._parakeet_cpp_model_status, 1) + self._parakeet_cpp_model_refresh_btn = QPushButton( + t("btn_refresh_parakeet_cpp_models") + ) + self._parakeet_cpp_model_refresh_btn.clicked.connect( + self._refresh_parakeet_cpp_models + ) + parakeet_model_layout.addWidget(self._parakeet_cpp_model_refresh_btn) + layout.addWidget(self._parakeet_cpp_model_group) + self._parakeet_cpp_model_group.setVisible( + self._selected_asr_engine() == "parakeet-cpp" + ) + + self._parakeet_cpp_runtime_group = QGroupBox(t("group_parakeet_cpp_runtime")) + parakeet_runtime_layout = QHBoxLayout(self._parakeet_cpp_runtime_group) + self._parakeet_cpp_runtime_combo = QComboBox() + saved_parakeet_runtime = s.get("parakeet_cpp_runtime_dir", "") + self._populate_parakeet_cpp_runtimes(saved_parakeet_runtime) + self._parakeet_cpp_runtime_combo.currentIndexChanged.connect( + self._on_parakeet_cpp_runtime_changed + ) + parakeet_runtime_layout.addWidget(self._parakeet_cpp_runtime_combo) + self._parakeet_cpp_runtime_status = QLabel("") + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #888; font-size: 11px;" + ) + parakeet_runtime_layout.addWidget(self._parakeet_cpp_runtime_status, 1) + self._parakeet_cpp_runtime_refresh_btn = QPushButton( + t("btn_refresh_parakeet_cpp_runtimes") + ) + self._parakeet_cpp_runtime_refresh_btn.clicked.connect( + self._refresh_parakeet_cpp_runtimes + ) + parakeet_runtime_layout.addWidget(self._parakeet_cpp_runtime_refresh_btn) + layout.addWidget(self._parakeet_cpp_runtime_group) + self._parakeet_cpp_runtime_group.setVisible( + self._selected_asr_engine() == "parakeet-cpp" + ) + # Remote ASR server URL — only visible when engine is Remote Whisper self._remote_group = QGroupBox("Remote ASR Server") remote_layout = QHBoxLayout(self._remote_group) @@ -472,6 +567,8 @@ def _create_vad_tab(self): self._update_whisper_size_label() self._update_crispasr_status() self._update_sherpa_onnx_status() + self._update_parakeet_cpp_model_status() + self._update_parakeet_cpp_runtime_status() mode_group = QGroupBox(t("group_vad_mode")) mode_layout = QVBoxLayout(mode_group) @@ -1234,11 +1331,16 @@ def _on_asr_engine_changed(self, index): is_remote = engine == "remote-whisper" is_crispasr = engine == "crispasr" is_sherpa_onnx = engine == "sherpa-onnx" + is_parakeet_cpp = engine == "parakeet-cpp" self._whisper_group.setVisible(is_whisper) if hasattr(self, "_crispasr_group"): self._crispasr_group.setVisible(is_crispasr) if hasattr(self, "_sherpa_onnx_group"): self._sherpa_onnx_group.setVisible(is_sherpa_onnx) + if hasattr(self, "_parakeet_cpp_model_group"): + self._parakeet_cpp_model_group.setVisible(is_parakeet_cpp) + if hasattr(self, "_parakeet_cpp_runtime_group"): + self._parakeet_cpp_runtime_group.setVisible(is_parakeet_cpp) if hasattr(self, "_funasr_model_combo"): self._funasr_model_label.setVisible(is_funasr) self._funasr_model_combo.setVisible(is_funasr) @@ -1257,6 +1359,12 @@ def _on_asr_engine_changed(self, index): self._sherpa_onnx_num_threads.setVisible(is_sherpa_onnx) self._sherpa_onnx_decoding_label.setVisible(is_sherpa_onnx) self._sherpa_onnx_decoding_method.setVisible(is_sherpa_onnx) + if hasattr(self, "_parakeet_cpp_backend"): + self._parakeet_cpp_backend_label.setVisible(is_parakeet_cpp) + self._parakeet_cpp_backend.setVisible(is_parakeet_cpp) + self._parakeet_cpp_decoder_label.setVisible(is_parakeet_cpp) + self._parakeet_cpp_decoder.setVisible(is_parakeet_cpp) + self._parakeet_cpp_word_timestamps.setVisible(is_parakeet_cpp) if hasattr(self, "_whisper_pad_seconds"): self._whisper_pad_label.setVisible(is_whisper) self._whisper_pad_seconds.setVisible(is_whisper) @@ -1346,6 +1454,49 @@ def _on_sherpa_onnx_setting_changed(self): ) self._auto_save() + def _selected_parakeet_cpp_model(self) -> str: + value = self._parakeet_cpp_model_combo.currentData() + return str(value) if value is not None else "" + + def _selected_parakeet_cpp_runtime_dir(self) -> str: + value = self._parakeet_cpp_runtime_combo.currentData() + return str(value) if value is not None else "" + + def _selected_parakeet_cpp_backend(self) -> str: + value = self._parakeet_cpp_backend.currentData() + return str(value) if value else "auto" + + def _selected_parakeet_cpp_decoder(self) -> str: + value = self._parakeet_cpp_decoder.currentData() + return str(value) if value else "auto" + + def _on_parakeet_cpp_model_changed(self): + self._current_settings["parakeet_cpp_model"] = ( + self._selected_parakeet_cpp_model() + ) + self._update_parakeet_cpp_model_status() + self._auto_save() + + def _on_parakeet_cpp_runtime_changed(self): + self._current_settings["parakeet_cpp_runtime_dir"] = ( + self._selected_parakeet_cpp_runtime_dir() + ) + self._update_parakeet_cpp_runtime_status() + self._auto_save() + + def _on_parakeet_cpp_setting_changed(self): + self._current_settings["parakeet_cpp_backend"] = ( + self._selected_parakeet_cpp_backend() + ) + self._current_settings["parakeet_cpp_decoder"] = ( + self._selected_parakeet_cpp_decoder() + ) + self._current_settings["parakeet_cpp_word_timestamps"] = ( + self._parakeet_cpp_word_timestamps.isChecked() + ) + self._update_parakeet_cpp_runtime_status() + self._auto_save() + def _selected_firered_vad_model(self) -> str: value = self._firered_vad_model_combo.currentData() return str(value) if value is not None else "" @@ -1485,6 +1636,175 @@ def _update_sherpa_onnx_status(self): self._sherpa_onnx_status.setText(t("sherpa_onnx_invalid_local")) self._sherpa_onnx_status.setStyleSheet("color: #d66; font-size: 11px;") + def _populate_parakeet_cpp_models(self, saved_value: str): + self._parakeet_cpp_model_combo.clear() + self._parakeet_cpp_model_combo.addItem( + t("parakeet_cpp_model_placeholder"), "" + ) + + local_prefix = t("parakeet_cpp_local_prefix") + for item in list_local_parakeet_cpp_models(): + idx = self._parakeet_cpp_model_combo.count() + self._parakeet_cpp_model_combo.addItem( + f"{local_prefix}: {item['name']}", item["path"] + ) + self._parakeet_cpp_model_combo.setItemData( + idx, item["path"], Qt.ItemDataRole.ToolTipRole + ) + + if not saved_value: + selected = "" + else: + selected = resolve_custom_parakeet_cpp_model(saved_value) or saved_value + idx = self._parakeet_cpp_model_combo.findData(selected) + if idx < 0: + idx = self._parakeet_cpp_model_combo.findText(saved_value) + if idx < 0 and selected: + label = f"{t('parakeet_cpp_missing_local')}: {Path(str(selected)).name}" + idx = self._parakeet_cpp_model_combo.count() + self._parakeet_cpp_model_combo.addItem(label, selected) + self._parakeet_cpp_model_combo.setItemData( + idx, str(selected), Qt.ItemDataRole.ToolTipRole + ) + if idx >= 0: + self._parakeet_cpp_model_combo.setCurrentIndex(idx) + + def _populate_parakeet_cpp_runtimes(self, saved_value: str): + self._parakeet_cpp_runtime_combo.clear() + self._parakeet_cpp_runtime_combo.addItem( + t("parakeet_cpp_runtime_placeholder"), "" + ) + + local_prefix = t("parakeet_cpp_local_prefix") + for item in list_local_parakeet_cpp_runtimes(): + idx = self._parakeet_cpp_runtime_combo.count() + backend = str(item.get("backend") or "").lower() + suffix = f" [{backend}]" if backend and backend != "unknown" else "" + self._parakeet_cpp_runtime_combo.addItem( + f"{local_prefix}: {item['name']}{suffix}", item["path"] + ) + self._parakeet_cpp_runtime_combo.setItemData( + idx, item["path"], Qt.ItemDataRole.ToolTipRole + ) + + if not saved_value: + selected = "" + else: + selected = ( + resolve_parakeet_cpp_runtime_dir(saved_value, "auto") + or saved_value + ) + idx = self._parakeet_cpp_runtime_combo.findData(selected) + if idx < 0: + idx = self._parakeet_cpp_runtime_combo.findText(saved_value) + if idx < 0 and selected: + label = f"{t('parakeet_cpp_missing_local')}: {Path(str(selected)).name}" + idx = self._parakeet_cpp_runtime_combo.count() + self._parakeet_cpp_runtime_combo.addItem(label, selected) + self._parakeet_cpp_runtime_combo.setItemData( + idx, str(selected), Qt.ItemDataRole.ToolTipRole + ) + if idx >= 0: + self._parakeet_cpp_runtime_combo.setCurrentIndex(idx) + + def _refresh_parakeet_cpp_models(self): + saved = self._selected_parakeet_cpp_model() + self._populate_parakeet_cpp_models(saved) + self._update_parakeet_cpp_model_status() + + def _refresh_parakeet_cpp_runtimes(self): + saved = self._selected_parakeet_cpp_runtime_dir() + self._populate_parakeet_cpp_runtimes(saved) + self._update_parakeet_cpp_runtime_status() + + def _update_parakeet_cpp_model_status(self): + from model_manager import is_asr_cached + + model_key = self._selected_parakeet_cpp_model() + if not model_key: + if list_local_parakeet_cpp_models(): + self._parakeet_cpp_model_status.setText( + t("parakeet_cpp_select_model") + ) + else: + self._parakeet_cpp_model_status.setText( + t("parakeet_cpp_no_local_models") + ) + self._parakeet_cpp_model_status.setStyleSheet( + "color: #888; font-size: 11px;" + ) + return + cached = is_asr_cached( + "parakeet-cpp", model_key, self._current_settings.get("hub", "ms") + ) + if cached: + self._parakeet_cpp_model_status.setText( + t("parakeet_cpp_local_ready") + ) + self._parakeet_cpp_model_status.setStyleSheet( + "color: #4a4; font-size: 11px;" + ) + else: + self._parakeet_cpp_model_status.setText( + t("parakeet_cpp_invalid_local") + ) + self._parakeet_cpp_model_status.setStyleSheet( + "color: #d66; font-size: 11px;" + ) + + def _update_parakeet_cpp_runtime_status(self): + from model_manager import detect_parakeet_cpp_runtime_dir + + runtime_dir = self._selected_parakeet_cpp_runtime_dir() + backend = self._selected_parakeet_cpp_backend() + if not runtime_dir: + if list_local_parakeet_cpp_runtimes(): + self._parakeet_cpp_runtime_status.setText( + t("parakeet_cpp_select_runtime") + ) + else: + self._parakeet_cpp_runtime_status.setText( + t("parakeet_cpp_no_local_runtimes") + ) + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #888; font-size: 11px;" + ) + return + resolved = resolve_parakeet_cpp_runtime_dir(runtime_dir, "auto") + info = detect_parakeet_cpp_runtime_dir(resolved) if resolved else None + if not info: + self._parakeet_cpp_runtime_status.setText( + t("parakeet_cpp_invalid_runtime") + ) + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #d66; font-size: 11px;" + ) + return + runtime_backend = str(info.get("backend") or "unknown") + missing = info.get("missing_dependencies") or [] + if backend != "auto" and runtime_backend not in ("unknown", backend): + self._parakeet_cpp_runtime_status.setText( + t("parakeet_cpp_invalid_runtime") + ) + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #d66; font-size: 11px;" + ) + return + if missing: + self._parakeet_cpp_runtime_status.setText( + f"{t('parakeet_cpp_runtime_ready')} ({', '.join(missing)}?)" + ) + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #d99; font-size: 11px;" + ) + return + self._parakeet_cpp_runtime_status.setText( + t("parakeet_cpp_runtime_ready") + ) + self._parakeet_cpp_runtime_status.setStyleSheet( + "color: #4a4; font-size: 11px;" + ) + def _populate_crispasr_models(self, saved_value: str): self._crispasr_model_combo.clear() self._crispasr_model_combo.addItem(t("crispasr_model_placeholder"), "") @@ -1934,6 +2254,21 @@ def _apply_settings(self): self._current_settings["sherpa_onnx_decoding_method"] = ( self._selected_sherpa_onnx_decoding_method() ) + self._current_settings["parakeet_cpp_model"] = ( + self._selected_parakeet_cpp_model() + ) + self._current_settings["parakeet_cpp_runtime_dir"] = ( + self._selected_parakeet_cpp_runtime_dir() + ) + self._current_settings["parakeet_cpp_backend"] = ( + self._selected_parakeet_cpp_backend() + ) + self._current_settings["parakeet_cpp_decoder"] = ( + self._selected_parakeet_cpp_decoder() + ) + self._current_settings["parakeet_cpp_word_timestamps"] = ( + self._parakeet_cpp_word_timestamps.isChecked() + ) dev_text = self._asr_device.currentText() self._current_settings["asr_device"] = dev_text.split(" (")[0] audio_idx = self._audio_device.currentIndex() diff --git a/i18n/en.yaml b/i18n/en.yaml index c71d892..99e9418 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -90,6 +90,11 @@ label_sherpa_onnx_model: "sherpa-onnx model:" label_sherpa_onnx_provider: "sherpa-onnx provider:" label_sherpa_onnx_threads: "sherpa-onnx threads:" label_sherpa_onnx_decoding: "sherpa-onnx decoding:" +label_parakeet_cpp_model: "parakeet.cpp model:" +label_parakeet_cpp_runtime: "Runtime:" +label_parakeet_cpp_backend: "Backend:" +label_parakeet_cpp_decoder: "Decoder:" +label_parakeet_cpp_word_timestamps: "Word timestamps" label_whisper_padding: "Whisper padding:" whisper_padding_off: "Off" whisper_padding_tooltip: "Pads faster-whisper input to this duration bucket. 0 disables padding." @@ -192,6 +197,22 @@ sherpa_onnx_local_prefix: "Local" sherpa_onnx_local_ready: "Local model" sherpa_onnx_missing_local: "Missing" sherpa_onnx_invalid_local: "Local model unavailable" +group_parakeet_cpp_model: "parakeet.cpp Model" +group_parakeet_cpp_runtime: "parakeet.cpp Runtime" +btn_refresh_parakeet_cpp_models: "Refresh" +btn_refresh_parakeet_cpp_runtimes: "Refresh" +parakeet_cpp_model_placeholder: "Select parakeet.cpp GGUF model" +parakeet_cpp_runtime_placeholder: "Select parakeet.cpp runtime" +parakeet_cpp_select_model: "Select a model" +parakeet_cpp_select_runtime: "Select a runtime" +parakeet_cpp_no_local_models: "No local parakeet.cpp models found" +parakeet_cpp_no_local_runtimes: "No local parakeet.cpp runtime found" +parakeet_cpp_local_prefix: "Local" +parakeet_cpp_local_ready: "Local model" +parakeet_cpp_runtime_ready: "Runtime ready" +parakeet_cpp_missing_local: "Missing" +parakeet_cpp_invalid_local: "Local model unavailable" +parakeet_cpp_invalid_runtime: "Runtime unavailable" btn_open_folder: "Open Models Folder" btn_delete_all_exit: "Delete All && Exit" scanning: "Scanning..." diff --git a/i18n/zh.yaml b/i18n/zh.yaml index d7528b9..2d4453f 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -90,6 +90,11 @@ label_sherpa_onnx_model: "sherpa-onnx 模型:" label_sherpa_onnx_provider: "sherpa-onnx Provider:" label_sherpa_onnx_threads: "sherpa-onnx 线程:" label_sherpa_onnx_decoding: "sherpa-onnx 解码:" +label_parakeet_cpp_model: "parakeet.cpp 模型:" +label_parakeet_cpp_runtime: "Runtime:" +label_parakeet_cpp_backend: "Backend:" +label_parakeet_cpp_decoder: "Decoder:" +label_parakeet_cpp_word_timestamps: "词级时间戳" label_whisper_padding: "Whisper 补零:" whisper_padding_off: "关闭" whisper_padding_tooltip: "将 faster-whisper 输入补齐到指定秒数档位。0 表示关闭。" @@ -192,6 +197,22 @@ sherpa_onnx_local_prefix: "本地" sherpa_onnx_local_ready: "本地模型" sherpa_onnx_missing_local: "缺失" sherpa_onnx_invalid_local: "本地模型不可用" +group_parakeet_cpp_model: "parakeet.cpp 模型" +group_parakeet_cpp_runtime: "parakeet.cpp Runtime" +btn_refresh_parakeet_cpp_models: "刷新" +btn_refresh_parakeet_cpp_runtimes: "刷新" +parakeet_cpp_model_placeholder: "请选择 parakeet.cpp GGUF 模型" +parakeet_cpp_runtime_placeholder: "请选择 parakeet.cpp runtime" +parakeet_cpp_select_model: "请选择模型" +parakeet_cpp_select_runtime: "请选择 runtime" +parakeet_cpp_no_local_models: "未扫描到本地 parakeet.cpp 模型" +parakeet_cpp_no_local_runtimes: "未扫描到本地 parakeet.cpp runtime" +parakeet_cpp_local_prefix: "本地" +parakeet_cpp_local_ready: "本地模型" +parakeet_cpp_runtime_ready: "Runtime ready" +parakeet_cpp_missing_local: "缺失" +parakeet_cpp_invalid_local: "本地模型不可用" +parakeet_cpp_invalid_runtime: "Runtime 不可用" btn_open_folder: "打开模型文件夹" btn_delete_all_exit: "删除全部并退出" scanning: "扫描中..." diff --git a/install.ps1 b/install.ps1 index 0d7379a..9a78351 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,18 @@ param( [switch]$DownloadFireRedVAD, [ValidateSet("ms", "hf")] - [string]$FireRedVADHub = "ms" + [string]$FireRedVADHub = "ms", + + [switch]$InstallParakeetCpp, + + [ValidateSet("cpu", "cuda", "vulkan")] + [string]$ParakeetCppBackend = "cpu", + + [string]$ParakeetCppVersion = "v0.3.2", + + [switch]$DownloadParakeetCppModel, + + [string]$ParakeetCppModel = "tdt_ctc-110m-q4_k" ) $ErrorActionPreference = "Stop" @@ -148,6 +159,82 @@ function Download-FireRedVADModel { } } +function Install-ParakeetCppRuntime { + param( + [string]$PythonExe, + [string]$Backend, + [string]$Version + ) + + Write-Step "Installing parakeet.cpp runtime ($Backend, $Version)..." + $versionNoV = $Version.TrimStart("v") + $asset = "parakeet-$Version-lib-win-$Backend-x64.zip" + $url = "https://github.com/mudler/parakeet.cpp/releases/download/$Version/$asset" + $target = Join-Path $ProjectDir "models\parakeet.cpp\runtime\$Version\$Backend" + $tmpDir = Join-Path $env:TEMP "livetranslate-parakeet-$Backend" + $archive = Join-Path $tmpDir $asset + try { + if (Test-Path $tmpDir) { Remove-Item -Recurse -Force $tmpDir } + New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null + New-Item -ItemType Directory -Force -Path $target | Out-Null + + Write-Host " Downloading $asset" -ForegroundColor Gray + Invoke-WebRequest -Uri $url -OutFile $archive + Expand-Archive -Path $archive -DestinationPath $tmpDir -Force + + $dll = Get-ChildItem -Path $tmpDir -Recurse -Filter "*.dll" | + Where-Object { $_.Name -in @("parakeet.dll", "libparakeet.dll", "parakeet_capi.dll", "libparakeet_capi.dll") } | + Select-Object -First 1 + if (-not $dll) { + throw "parakeet C API DLL not found in $asset" + } + + Copy-Item -Path (Join-Path $tmpDir "*") -Destination $target -Recurse -Force + + if ($Backend -eq "cuda") { + $cudartAsset = "cudart-parakeet-bin-win-cuda-x64.zip" + $cudartUrl = "https://github.com/mudler/parakeet.cpp/releases/download/$Version/$cudartAsset" + $cudartArchive = Join-Path $tmpDir $cudartAsset + Write-Host " Downloading $cudartAsset" -ForegroundColor Gray + Invoke-WebRequest -Uri $cudartUrl -OutFile $cudartArchive + Expand-Archive -Path $cudartArchive -DestinationPath $target -Force + } + + $test = & $PythonExe -c "import ctypes, pathlib; root=pathlib.Path(r'$target'); names=('parakeet.dll','libparakeet.dll','parakeet_capi.dll','libparakeet_capi.dll'); dll=next((p for n in names for p in root.rglob(n)), None); assert dll, 'DLL not found'; lib=ctypes.CDLL(str(dll)); lib.parakeet_capi_abi_version.restype=ctypes.c_int; print(lib.parakeet_capi_abi_version())" 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Runtime smoke test failed: $test" + } + Write-Ok "parakeet.cpp runtime ready: models\parakeet.cpp\runtime\$Version\$Backend (ABI $($test.Trim()))" + } catch { + Write-Warn "parakeet.cpp runtime installation failed: $($_.Exception.Message)" + Write-Warn "You can manually extract $asset to models\parakeet.cpp\runtime\$Version\$Backend" + } +} + +function Download-ParakeetCppModel { + param( + [string]$PythonExe, + [string]$ModelName + ) + + Write-Step "Downloading parakeet.cpp GGUF model ($ModelName)..." + $target = Join-Path $ProjectDir "models\parakeet.cpp\models" + New-Item -ItemType Directory -Force -Path $target | Out-Null + $fileName = if ($ModelName.EndsWith(".gguf")) { $ModelName } else { "$ModelName.gguf" } + try { + & $PythonExe -c "from huggingface_hub import hf_hub_download; hf_hub_download(repo_id='mudler/parakeet-cpp-gguf', filename=r'$fileName', local_dir=r'models/parakeet.cpp/models')" + if ($LASTEXITCODE -ne 0) { throw "HuggingFace model download command failed" } + $modelPath = Join-Path $target $fileName + if (-not (Test-Path $modelPath)) { + throw "Downloaded model file not found: $modelPath" + } + Write-Ok "parakeet.cpp model ready: models\parakeet.cpp\models\$fileName" + } catch { + Write-Warn "parakeet.cpp model download failed: $($_.Exception.Message)" + Write-Warn "Download a GGUF from mudler/parakeet-cpp-gguf and place it under models\parakeet.cpp\models" + } +} + function Enable-SystemProxy { # uv (Python download) and pip honor *_PROXY env vars but not the Windows # registry system proxy; bridge it here. An already-set env proxy wins. @@ -482,6 +569,15 @@ if ($LASTEXITCODE -ne 0) { Write-Ok "FunASR installed" } +# ── Step 10: Optional parakeet.cpp runtime/model ── +if ($InstallParakeetCpp) { + Install-ParakeetCppRuntime -PythonExe $Python -Backend $ParakeetCppBackend -Version $ParakeetCppVersion +} + +if ($DownloadParakeetCppModel) { + Download-ParakeetCppModel -PythonExe $Python -ModelName $ParakeetCppModel +} + # ── Done ── Write-Host "" Write-Host "========================================" -ForegroundColor Green diff --git a/main.py b/main.py index d8c5489..e2f651e 100644 --- a/main.py +++ b/main.py @@ -260,6 +260,11 @@ def _on_settings_changed(self, settings): "sherpa_onnx_provider", "sherpa_onnx_num_threads", "sherpa_onnx_decoding_method", + "parakeet_cpp_model", + "parakeet_cpp_runtime_dir", + "parakeet_cpp_backend", + "parakeet_cpp_decoder", + "parakeet_cpp_word_timestamps", "remote_asr_url", "hub", ) @@ -753,6 +758,11 @@ def main(): config["asr"].setdefault("sherpa_onnx_provider", "auto") config["asr"].setdefault("sherpa_onnx_num_threads", 2) config["asr"].setdefault("sherpa_onnx_decoding_method", "greedy_search") + config["asr"].setdefault("parakeet_cpp_model", "") + config["asr"].setdefault("parakeet_cpp_runtime_dir", "") + config["asr"].setdefault("parakeet_cpp_backend", "auto") + config["asr"].setdefault("parakeet_cpp_decoder", "auto") + config["asr"].setdefault("parakeet_cpp_word_timestamps", True) saved = load_settings(config) migrate_funasr_settings(saved) @@ -845,6 +855,11 @@ def main(): "sherpa_onnx_model", config["asr"].get("sherpa_onnx_model", ""), ) + elif current_engine == "parakeet-cpp": + startup_model_key = saved.get( + "parakeet_cpp_model", + config["asr"].get("parakeet_cpp_model", ""), + ) else: startup_model_key = saved.get("whisper_model_size", config["asr"]["model_size"]) missing = get_missing_models( diff --git a/model_manager.py b/model_manager.py index 413c61a..64edc4c 100644 --- a/model_manager.py +++ b/model_manager.py @@ -109,6 +109,7 @@ def _proxy_env(proxy: str): DEFAULT_FUNASR_MODEL = "sensevoice-small" DEFAULT_SHERPA_ONNX_MODEL = "" DEFAULT_FIRERED_VAD_MODEL = "" +DEFAULT_PARAKEET_CPP_MODEL = "" FUNASR_LEGACY_ENGINE_ALIASES = { "sensevoice": "sensevoice-small", @@ -144,11 +145,32 @@ def asr_model_id( "anime-whisper": "Anime-Whisper", "crispasr": "CrispASR", "sherpa-onnx": "sherpa-onnx", + "parakeet-cpp": "parakeet.cpp", "remote-whisper": "Remote-Whisper", } _CRISPASR_EXTS = {".gguf", ".bin"} _CRISPASR_MIN_BYTES = 1_000_000 +_PARAKEET_CPP_MIN_BYTES = 1_000_000 +_PARAKEET_CPP_MODEL_PREFIXES = ( + "tdt_ctc-110m", + "tdt_ctc-1.1b", + "tdt-0.6b-v2", + "tdt-0.6b-v3", + "tdt-1.1b", + "ctc-0.6b", + "ctc-1.1b", + "rnnt-0.6b", + "rnnt-1.1b", + "realtime_eou_120m-v1", + "nemotron-3.5-asr-streaming-0.6b", +) +_PARAKEET_CPP_LIBRARY_NAMES = ( + "parakeet.dll", + "libparakeet.dll", + "parakeet_capi.dll", + "libparakeet_capi.dll", +) _MODEL_SIZE_BYTES = { "silero-vad": 2_000_000, @@ -230,6 +252,288 @@ def funasr_model_id(model_key: str | None, hub: str = "ms") -> str: return profile["huggingface_id"] if hub == "hf" else profile["modelscope_id"] +def _custom_parakeet_cpp_path(value) -> Path | None: + if not value: + return None + path = Path(str(value)).expanduser() + if not path.is_absolute(): + path = APP_DIR / path + return path + + +def _read_parakeet_cpp_sidecar(path: Path) -> dict: + candidates = [ + path.with_suffix(path.suffix + ".json"), + path.with_suffix(".json"), + path.parent / "parakeet_cpp_model.json", + ] + for metadata_path in candidates: + if not metadata_path.is_file(): + continue + try: + data = json.loads(metadata_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + log.warning(f"Invalid parakeet.cpp metadata: {metadata_path}: {exc}") + return {} + return data if isinstance(data, dict) else {} + return {} + + +def _parakeet_cpp_name_hint(path: Path) -> dict | None: + name = path.name.lower() + stem = path.stem.lower() + if not any(stem.startswith(prefix) for prefix in _PARAKEET_CPP_MODEL_PREFIXES): + return None + tags = [] + if "nemotron" in stem: + tags.append("multilingual") + if "realtime_eou" in stem or "streaming" in stem: + tags.append("streaming/eou") + return { + "display_name": stem.replace("_", " "), + "decoder": "auto", + "language": "auto" if "nemotron" in stem else "en", + "tags": tags, + "filename": name, + } + + +def detect_parakeet_cpp_model_file(path) -> dict | None: + """Return normalized parakeet.cpp metadata for a known GGUF model file.""" + if not path: + return None + path = Path(path) + if not path.is_file() or path.suffix.lower() != ".gguf": + return None + try: + if path.stat().st_size < _PARAKEET_CPP_MIN_BYTES: + return None + path = path.resolve() + except OSError: + return None + + metadata = _read_parakeet_cpp_sidecar(path) + family = str(metadata.get("family") or "").strip().lower().replace("-", "_") + architecture = str( + metadata.get("architecture") + or metadata.get("gguf.architecture") + or metadata.get("gguf_architecture") + or "" + ).strip().lower() + model_file = metadata.get("model_file") + if model_file: + candidate = Path(str(model_file)) + if not candidate.is_absolute(): + candidate = path.parent / candidate + try: + if candidate.resolve() != path: + return None + except OSError: + return None + + hint = _parakeet_cpp_name_hint(path) + if family in ("parakeet_cpp", "parakeet.cpp") or architecture == "parakeet": + display = metadata.get("display_name") or metadata.get("name") + return { + "path": str(path), + "display_name": str(display).strip() if display else (hint or {}) .get("display_name", path.name), + "decoder": str(metadata.get("decoder") or (hint or {}).get("decoder") or "auto"), + "language": str(metadata.get("language") or (hint or {}).get("language") or "auto"), + "tags": metadata.get("tags") if isinstance(metadata.get("tags"), list) else (hint or {}).get("tags", []), + } + if hint: + return {"path": str(path), **hint} + return None + + +def is_parakeet_cpp_model_file(path) -> bool: + return detect_parakeet_cpp_model_file(path) is not None + + +def resolve_custom_parakeet_cpp_model(value) -> str | None: + path = _custom_parakeet_cpp_path(value) + if path and is_parakeet_cpp_model_file(path): + return str(path.resolve()) + return None + + +def list_local_parakeet_cpp_models() -> list[dict]: + """Scan ./models for recognizable parakeet.cpp GGUF model files.""" + if not MODELS_DIR.exists(): + return [] + + entries = [] + name_counts = {} + seen = set() + try: + files = list(MODELS_DIR.rglob("*.gguf")) + except (OSError, PermissionError): + return [] + + for path in files: + info = detect_parakeet_cpp_model_file(path) + if not info: + continue + identity = info["path"] + if identity in seen: + continue + seen.add(identity) + name = str(info.get("display_name") or path.name) + tags = info.get("tags") or [] + if tags: + name = f"{name} [{' / '.join(str(tag) for tag in tags)}]" + name_counts[name] = name_counts.get(name, 0) + 1 + if name_counts[name] > 1: + name = f"{path.stem} ({path.parent.name}){path.suffix}" + entries.append( + { + "name": name, + "path": identity, + "decoder": info.get("decoder", "auto"), + "language": info.get("language", "auto"), + "info": info, + } + ) + + entries.sort(key=lambda item: item["name"].lower()) + return entries + + +def get_parakeet_cpp_model_path(value) -> str | None: + return resolve_custom_parakeet_cpp_model(value) + + +def local_parakeet_cpp_display_name(path) -> str | None: + resolved = resolve_custom_parakeet_cpp_model(path) + if not resolved: + return None + for item in list_local_parakeet_cpp_models(): + if item["path"] == resolved: + return item["name"] + info = detect_parakeet_cpp_model_file(resolved) + return info["display_name"] if info else Path(resolved).name + + +def _custom_parakeet_cpp_runtime_path(value) -> Path | None: + if not value: + return None + path = Path(str(value)).expanduser() + if not path.is_absolute(): + path = APP_DIR / path + return path + + +def _find_parakeet_cpp_library(path: Path) -> Path | None: + for name in _PARAKEET_CPP_LIBRARY_NAMES: + candidate = path / name + if candidate.is_file(): + return candidate + for name in _PARAKEET_CPP_LIBRARY_NAMES: + matches = list(path.rglob(name)) + if matches: + return matches[0] + return None + + +def _parakeet_cpp_runtime_backend_hint(path: Path) -> str: + lowered = " ".join(part.lower() for part in path.parts) + if "cuda" in lowered: + return "cuda" + if "vulkan" in lowered: + return "vulkan" + if "cpu" in lowered: + return "cpu" + return "unknown" + + +def detect_parakeet_cpp_runtime_dir(path) -> dict | None: + if not path: + return None + path = Path(path) + if not path.is_dir(): + return None + try: + path = path.resolve() + except OSError: + return None + library = _find_parakeet_cpp_library(path) + if not library: + return None + backend = _parakeet_cpp_runtime_backend_hint(path) + missing = [] + if backend == "cuda": + has_cudart = any(path.rglob("cudart*.dll")) + if not has_cudart: + missing.append("cudart*.dll") + return { + "path": str(path), + "library": str(library), + "backend": backend, + "display_name": parakeet_cpp_runtime_display_name(path), + "missing_dependencies": missing, + } + + +def resolve_parakeet_cpp_runtime_dir(value, backend: str = "auto") -> str | None: + path = _custom_parakeet_cpp_runtime_path(value) + if path: + info = detect_parakeet_cpp_runtime_dir(path) + if info and ( + backend in ("", "auto") + or info["backend"] in ("unknown", backend) + ): + return info["path"] + return None + + +def list_local_parakeet_cpp_runtimes() -> list[dict]: + if not MODELS_DIR.exists(): + return [] + + runtime_root = MODELS_DIR / "parakeet.cpp" / "runtime" + candidates = [] + try: + if any((MODELS_DIR / name).is_file() for name in _PARAKEET_CPP_LIBRARY_NAMES): + candidates.append(MODELS_DIR) + candidates.extend( + path + for path in MODELS_DIR.iterdir() + if path.is_dir() and "parakeet" in path.name.lower() + ) + if runtime_root.exists(): + candidates.append(runtime_root) + candidates.extend(path for path in runtime_root.rglob("*") if path.is_dir()) + except (OSError, PermissionError): + return [] + + entries = [] + seen = set() + for path in candidates: + info = detect_parakeet_cpp_runtime_dir(path) + if not info or info["path"] in seen: + continue + seen.add(info["path"]) + entries.append( + { + "name": info["display_name"], + "path": info["path"], + "backend": info["backend"], + "info": info, + } + ) + entries.sort(key=lambda item: item["name"].lower()) + return entries + + +def parakeet_cpp_runtime_display_name(path) -> str: + path = Path(path) + backend = _parakeet_cpp_runtime_backend_hint(path) + name = path.name + if backend != "unknown" and backend not in name.lower(): + return f"{name} [{backend}]" + return name + + def is_crispasr_model_file(path) -> bool: if not path: return False @@ -239,6 +543,7 @@ def is_crispasr_model_file(path) -> bool: path.is_file() and path.suffix.lower() in _CRISPASR_EXTS and path.stat().st_size >= _CRISPASR_MIN_BYTES + and not is_parakeet_cpp_model_file(path) ) except OSError: return False @@ -938,6 +1243,8 @@ def is_asr_cached(engine_type, model_size="medium", hub="ms") -> bool: return resolve_custom_crispasr_model(model_size) is not None if engine_type == "sherpa-onnx": return get_sherpa_onnx_model_path(model_size) is not None + if engine_type == "parakeet-cpp": + return get_parakeet_cpp_model_path(model_size) is not None if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -999,7 +1306,7 @@ def get_missing_models(engine, model_size, hub) -> list: if not is_asr_cached(engine, model_size, hub): if engine == "whisper" and model_size not in _WHISPER_SIZES: return missing - if engine in ("crispasr", "sherpa-onnx"): + if engine in ("crispasr", "sherpa-onnx", "parakeet-cpp"): return missing if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -1045,6 +1352,9 @@ def get_local_model_path( if engine_type == "sherpa-onnx": model_value = model_path_or_id if model_path_or_id is not None else funasr_model return get_sherpa_onnx_model_path(model_value) + if engine_type == "parakeet-cpp": + model_value = model_path_or_id if model_path_or_id is not None else funasr_model + return get_parakeet_cpp_model_path(model_value) if engine_type == "funasr" or engine_type in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( @@ -1240,6 +1550,12 @@ def get_cache_entries(): for item in list_local_sherpa_onnx_models(): entries.append((f"sherpa-onnx Local: {item['name']}", Path(item["path"]))) + for item in list_local_parakeet_cpp_models(): + entries.append((f"parakeet.cpp Local: {item['name']}", Path(item["path"]))) + + for item in list_local_parakeet_cpp_runtimes(): + entries.append((f"parakeet.cpp Runtime: {item['name']}", Path(item["path"]))) + for item in list_local_firered_vad_models(): entries.append((f"FireRedVAD Local: {item['name']}", Path(item["path"]))) diff --git a/settings_store.py b/settings_store.py index 8d41189..0404218 100644 --- a/settings_store.py +++ b/settings_store.py @@ -6,6 +6,7 @@ from model_manager import ( DEFAULT_FIRERED_VAD_MODEL, DEFAULT_FUNASR_MODEL, + DEFAULT_PARAKEET_CPP_MODEL, migrate_funasr_settings, normalize_funasr_model_key, ) @@ -85,6 +86,20 @@ def normalize_settings(config: dict[str, Any], saved: dict | None = None) -> dic "sherpa_onnx_tail_padding_seconds", asr.get("sherpa_onnx_tail_padding_seconds", 0.5), ) + settings.setdefault( + "parakeet_cpp_model", + asr.get("parakeet_cpp_model", DEFAULT_PARAKEET_CPP_MODEL), + ) + settings.setdefault( + "parakeet_cpp_runtime_dir", + asr.get("parakeet_cpp_runtime_dir", ""), + ) + settings.setdefault("parakeet_cpp_backend", asr.get("parakeet_cpp_backend", "auto")) + settings.setdefault("parakeet_cpp_decoder", asr.get("parakeet_cpp_decoder", "auto")) + settings.setdefault( + "parakeet_cpp_word_timestamps", + asr.get("parakeet_cpp_word_timestamps", True), + ) settings.setdefault("asr_device", asr.get("device", "cuda")) settings.setdefault( "sensevoice_pad_seconds", asr.get("sensevoice_pad_seconds", 0.5) From 04800fb678686242971a0300ff37bc263e2e5cc1 Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sun, 21 Jun 2026 06:29:10 +0800 Subject: [PATCH 12/13] =?UTF-8?q?feat(ui):=20=E6=8B=86=E5=88=86=20VAD=20?= =?UTF-8?q?=E4=B8=8E=20ASR=20=E8=AE=BE=E7=BD=AE=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将控制面板原 VAD/ASR 标签拆为独立 ASR 和 VAD 标签,降低单页配置密度。 - ASR 页保留引擎、模型、设备、Hub、界面语言和远程 ASR URL 等现有控件。 - VAD 页承载 VAD 模式、神经阈值、FireRedVAD、本地能量阈值和分段时序设置。 - 保持原有设置 key、自动保存逻辑、运行时信号和模型状态刷新流程不变。 - 补充中英文标签文案,并同步 CLAUDE 架构说明为 8 个设置标签。 --- CLAUDE.md | 2 +- control_panel.py | 17 ++++++++++++++--- i18n/en.yaml | 2 ++ i18n/zh.yaml | 2 ++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 80c6c4a..ce5c5c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ main.py (LiveTranslateApp) |-- subtitle_overlay.py PyQt6 transparent overlay (2-row header: controls + model/lang combos) |-- subtitle_window.py Standalone subtitle window for OBS capture (outlined text, animations) |-- subtitle_settings.py Subtitle window settings UI (grid layout, text line editor) - |-- control_panel.py Settings UI (7 tabs: VAD/ASR, Translation, Style, Subtitle, Benchmark, Cache, Changelog) + |-- control_panel.py Settings UI (8 tabs: ASR, VAD, Translation, Style, Subtitle, Benchmark, Cache, Changelog) |-- dialogs.py Setup wizard, model download/load dialogs, ModelEditDialog |-- benchmark.py Translation benchmark (BENCH_SENTENCES, run_benchmark()) |-- log_window.py Real-time log viewer diff --git a/control_panel.py b/control_panel.py index eb5d7cf..7718172 100644 --- a/control_panel.py +++ b/control_panel.py @@ -101,7 +101,8 @@ def __init__(self, config, saved_settings=None): layout = QVBoxLayout(self) tabs = QTabWidget() - tabs.addTab(self._create_vad_tab(), t("tab_vad_asr")) + tabs.addTab(self._create_asr_tab(), t("tab_asr")) + tabs.addTab(self._create_vad_tab(), t("tab_vad")) tabs.addTab(self._create_translation_tab(), t("tab_translation")) tabs.addTab(self._create_style_tab(), t("tab_style")) tabs.addTab(self._create_subtitle_tab(), t("tab_subtitle")) @@ -123,9 +124,9 @@ def __init__(self, config, saved_settings=None): # Fit initial height based on whisper group visibility QTimer.singleShot(0, lambda: self.resize(self.width(), self.sizeHint().height() + 20)) - # ── VAD / ASR Tab ── + # ── ASR Tab ── - def _create_vad_tab(self): + def _create_asr_tab(self): widget = QWidget() layout = QVBoxLayout(widget) s = self._current_settings @@ -570,6 +571,16 @@ def _create_vad_tab(self): self._update_parakeet_cpp_model_status() self._update_parakeet_cpp_runtime_status() + layout.addStretch() + return widget + + # ── VAD Tab ── + + def _create_vad_tab(self): + widget = QWidget() + layout = QVBoxLayout(widget) + s = self._current_settings + mode_group = QGroupBox(t("group_vad_mode")) mode_layout = QVBoxLayout(mode_group) self._vad_mode = QComboBox() diff --git a/i18n/en.yaml b/i18n/en.yaml index 99e9418..3921800 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -32,6 +32,8 @@ source_label: "Source:" # Control Panel window_control_panel: "LiveTranslate - Control Panel" tab_vad_asr: "VAD / ASR" +tab_asr: "ASR" +tab_vad: "VAD" tab_translation: "Translation" tab_benchmark: "Benchmark" tab_cache: "Cache" diff --git a/i18n/zh.yaml b/i18n/zh.yaml index 2d4453f..24a5419 100644 --- a/i18n/zh.yaml +++ b/i18n/zh.yaml @@ -32,6 +32,8 @@ source_label: "来源:" # 控制面板 window_control_panel: "LiveTranslate - 控制面板" tab_vad_asr: "VAD / ASR" +tab_asr: "ASR" +tab_vad: "VAD" tab_translation: "翻译" tab_benchmark: "基准测试" tab_cache: "缓存" From 37a7a62867cb42a450ebfcdb2cb5120238abbf3a Mon Sep 17 00:00:00 2001 From: amlkiller <2087663148@qq.com> Date: Sun, 21 Jun 2026 07:34:06 +0800 Subject: [PATCH 13/13] =?UTF-8?q?feat(asr):=20=E6=94=AF=E6=8C=81=20sherpa-?= =?UTF-8?q?onnx=20NeMo=20CTC=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 NeMo CTC family 的加载分支,调用 sherpa-onnx OfflineRecognizer.from_nemo_ctc,并传入模型、tokens、线程数、采样率、feature_dim 与解码方法。 模型检测支持 nemo_ctc/nemo-ctc 元数据别名,并在 sherpa-onnx 前缀目录中根据 model(.int8).onnx + tokens.txt 自动识别为 NeMo CTC;Paraformer 也优先匹配 model.int8.onnx。 CUDA 模式下将 torch/lib、crispasr 和 ctranslate2 加入当前进程 PATH,降低 sherpa-onnx CUDA DLL 加载失败概率;worker ready 消息同步上报 runtime_provider 便于诊断。 --- asr_sherpa_onnx.py | 17 +++++++++++++++++ asr_worker.py | 41 +++++++++++++++++++++++++++++++++++++++++ model_manager.py | 39 ++++++++++++++++++++++++++++----------- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/asr_sherpa_onnx.py b/asr_sherpa_onnx.py index 8d4197c..6690905 100644 --- a/asr_sherpa_onnx.py +++ b/asr_sherpa_onnx.py @@ -153,6 +153,23 @@ def _load_recognizer(self): ) return + if self.family == "nemo_ctc": + factory = self._sherpa_onnx.OfflineRecognizer.from_nemo_ctc + self._recognizer = _call_factory( + factory, + self.provider, + { + "model": self.model_info["model_file"], + "tokens": self.model_info["tokens_file"], + "num_threads": self.num_threads, + "sample_rate": self.sample_rate, + "feature_dim": int(self.model_info.get("feature_dim") or 80), + "decoding_method": self.decoding_method, + "debug": False, + }, + ) + return + if self.family == "moonshine": factory = self._sherpa_onnx.OfflineRecognizer.from_moonshine self._recognizer = _call_factory( diff --git a/asr_worker.py b/asr_worker.py index e9283f9..6fe05ca 100644 --- a/asr_worker.py +++ b/asr_worker.py @@ -4,6 +4,7 @@ import logging import os import sys +import sysconfig import traceback from typing import Any @@ -90,6 +91,44 @@ def _is_sherpa_onnx_cuda_load_error(exc: Exception) -> bool: ) +def _prepend_process_path(path: str) -> bool: + if not path or not os.path.isdir(path): + return False + current = os.environ.get("PATH", "") + parts = [p for p in current.split(os.pathsep) if p] + normalized = {os.path.normcase(os.path.abspath(p)) for p in parts} + target = os.path.normcase(os.path.abspath(path)) + if target in normalized: + return False + os.environ["PATH"] = path + (os.pathsep + current if current else "") + return True + + +def _prepare_sherpa_onnx_cuda_runtime(): + site_roots = [] + for key in ("purelib", "platlib"): + path = sysconfig.get_paths().get(key) + if path and path not in site_roots: + site_roots.append(path) + + added = [] + for root in site_roots: + for relative in ( + os.path.join("torch", "lib"), + "crispasr", + "ctranslate2", + ): + candidate = os.path.join(root, relative) + if _prepend_process_path(candidate): + added.append(candidate) + + if added: + log.info( + "sherpa-onnx CUDA DLL search path updated: " + + "; ".join(added) + ) + + def _load_engine(config: dict): from model_manager import MODELS_DIR, apply_cache_env @@ -146,6 +185,7 @@ def _load_engine(config: dict): ) if provider == "cuda": os.environ["CUDA_VISIBLE_DEVICES"] = str(device_index) + _prepare_sherpa_onnx_cuda_runtime() from asr_sherpa_onnx import SherpaOnnxEngine @@ -259,6 +299,7 @@ def worker_main(conn, config: dict): "engine_type": config.get("engine_type"), "display_name": config.get("display_name"), "device": config.get("device"), + "runtime_provider": getattr(engine, "provider", None), }, ) ) diff --git a/model_manager.py b/model_manager.py index 64edc4c..12948b9 100644 --- a/model_manager.py +++ b/model_manager.py @@ -771,21 +771,13 @@ def _sherpa_family_hint(path: Path, metadata: dict) -> str | None: "sense_voice": "sense_voice", "paraformer": "paraformer", "moonshine": "moonshine", + "nemo_ctc": "nemo_ctc", + "nemo-ctc": "nemo_ctc", "whisper": "whisper", "online_transducer": "online_transducer", "online-transducer": "online_transducer", } return aliases.get(family) - - name = path.name.lower().replace("-", "_") - if "sense_voice" in name or "sensevoice" in name: - return "sense_voice" - if "paraformer" in name: - return "paraformer" - if "moonshine" in name: - return "moonshine" - if name.startswith("sherpa_onnx_whisper") or name.startswith("sherpa-onnx-whisper"): - return "whisper" return None @@ -816,6 +808,20 @@ def _is_sherpa_online_transducer_dir(path: Path, metadata: dict, family: str | N return False +def _has_sherpa_prefix(path: Path) -> bool: + name = path.name.lower().replace("-", "_") + return name.startswith("sherpa_onnx") + + +def _is_sherpa_single_model_dir(path: Path, metadata: dict) -> bool: + model = ( + _sherpa_file(path, metadata, "model", "model_file") + or _first_glob_file(path, "model.int8.onnx", "model.onnx") + ) + tokens = _sherpa_file(path, metadata, "tokens", "tokens_file", default="tokens.txt") + return bool(model and tokens) + + def detect_sherpa_onnx_model_dir(path) -> dict | None: """Return normalized sherpa-onnx model metadata when a directory is usable.""" if not path: @@ -835,6 +841,8 @@ def detect_sherpa_onnx_model_dir(path) -> dict | None: family = _sherpa_family_hint(path, metadata) if family is None and _is_sherpa_online_transducer_dir(path, metadata, family): family = "online_transducer" + if family is None and _has_sherpa_prefix(path) and _is_sherpa_single_model_dir(path, metadata): + family = "nemo_ctc" tokens_file = _sherpa_file(path, metadata, "tokens", "tokens_file", default="tokens.txt") sample_rate = int(metadata.get("sample_rate") or 16000) feature_dim = int(metadata.get("feature_dim") or 80) @@ -859,7 +867,16 @@ def detect_sherpa_onnx_model_dir(path) -> dict | None: if family == "paraformer": model_file = ( _sherpa_file(path, metadata, "model", "model_file", "paraformer") - or _first_glob_file(path, "model.onnx") + or _first_glob_file(path, "model.int8.onnx", "model.onnx") + ) + if tokens_file and model_file: + return {**base, "tokens_file": tokens_file, "model_file": model_file} + return None + + if family == "nemo_ctc": + model_file = ( + _sherpa_file(path, metadata, "model", "model_file") + or _first_glob_file(path, "model.int8.onnx", "model.onnx") ) if tokens_file and model_file: return {**base, "tokens_file": tokens_file, "model_file": model_file}