diff --git a/CLAUDE.md b/CLAUDE.md index 913be9d..ce5c5c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,19 +24,24 @@ 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 + |-- 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 |-- 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) + |-- 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) |-- 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 @@ -45,12 +50,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 @@ -146,6 +151,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保留到下一段 @@ -165,13 +171,23 @@ 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. +- 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` - 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/README.md b/README.md index b983eea..3a8191e 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ 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, 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 - **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 @@ -71,23 +71,25 @@ 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 +uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" # 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. +> `uv sync` installs the optional FireRedVAD Python package; its model is downloaded separately only if you choose to use that VAD backend. @@ -97,6 +99,55 @@ pip install funasr --no-deps 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: + +```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. + +## 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: @@ -108,22 +159,28 @@ 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 ``` -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 ├── 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 +├── 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 @@ -138,7 +195,11 @@ 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 +- [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 ## Star History diff --git a/README_zh.md b/README_zh.md index ead159d..7e78422 100644 --- a/README_zh.md +++ b/README_zh.md @@ -21,13 +21,13 @@ Windows 实时音频翻译工具。捕获系统音频(WASAPI loopback)和可 ## 功能特性 - **实时翻译管线**:系统音频 → VAD → ASR → LLM 翻译 → 字幕显示 -- **多 ASR 引擎**:faster-whisper、SenseVoice、FunASR Nano、Anime-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 等 - **流式翻译显示**:翻译结果逐字实时显示 - **模型独立配置**:流式传输、结构化输出(JSON)、上下文历史、禁用思考 - **麦克风混音**:可选将麦克风输入混合到系统音频一起识别 -- **低延迟 VAD**:32ms 音频块 + Silero VAD,自适应静音检测 +- **低延迟 VAD**:32ms 音频块 + Silero VAD,可选 FireRedVAD,自适应静音检测 - **透明悬浮窗**:始终置顶、鼠标穿透、可拖拽,14 种配色主题 - **CUDA 加速**:ASR 模型 GPU 推理 - **模型自动管理**:首次启动向导,支持 ModelScope / HuggingFace 双源 @@ -71,23 +71,25 @@ 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 +uv pip install "sherpa-onnx>=1.13.3" "sherpa-onnx-bin>=1.13.3" # 启动 .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`。 +> `uv sync` 会安装可选的 FireRedVAD Python 包;FireRedVAD 模型只在你选择使用该 VAD 后端时单独下载。 @@ -97,6 +99,55 @@ pip install funasr --no-deps 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 替换,例如: + +```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 路线。 + +## 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 设置 → 翻译标签页: @@ -108,22 +159,28 @@ pip install funasr --no-deps | Model | `deepseek-chat` | | 代理 | `none` / `system` / 自定义地址 | +真实 API Key 会保存在已被 git 忽略的 `user_settings.json` 中,不要把真实凭据写入 `config.yaml`。 + ## 架构 ``` -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 后端 ├── 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 后端 +├── 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 模型下载与缓存管理 @@ -138,7 +195,11 @@ 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 调用 +- [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 后端 ## 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_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_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_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/asr_service.py b/asr_service.py new file mode 100644 index 0000000..e57ca5c --- /dev/null +++ b/asr_service.py @@ -0,0 +1,860 @@ +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, + 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, + get_sherpa_onnx_model_path, + 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, +) + +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._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 + 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 + + @property + def worker_pid(self) -> int | None: + with self._asr_lock: + return getattr(self._asr, "pid", None) + + 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), + ) + ) + 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 + 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", + self._config["asr"].get("remote_asr_url", "http://127.0.0.1:8765"), + ) + or "http://127.0.0.1:8765" + ).strip() + if not remote_asr_url: + remote_asr_url = "http://127.0.0.1:8765" + + 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 + else: + return self._invalid_plan( + engine_type, + device, + hub, + download_proxy, + 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 + 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 + + 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, + ) + 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, + ) + 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: + 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, + sherpa_onnx_model_path, + parakeet_cpp_model_path, + ) + 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, + "remote_asr_url": remote_asr_url, + } + 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, + } + ) + 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, + } + ) + 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, + "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, + "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 + if engine_type == "remote-whisper" + else device, + } + + 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 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, + "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"), + } + 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_engine_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_engine_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 _load_engine_client(self, worker_config: dict): + if worker_config.get("engine_type") == "remote-whisper": + from asr_remote import RemoteASREngine + + engine = RemoteASREngine( + server_url=worker_config.get("remote_asr_url") + or "http://127.0.0.1:8765" + ) + language = worker_config.get("language") + if language: + engine.set_language(language) + return engine + return self._load_asr_client(worker_config) + + 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._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 + + @staticmethod + def _display_name( + engine_type: str, + model_size: str, + model_path: str | None, + 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": + 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}" + 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}" + 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_sherpa_onnx.py b/asr_sherpa_onnx.py new file mode 100644 index 0000000..6690905 --- /dev/null +++ b/asr_sherpa_onnx.py @@ -0,0 +1,385 @@ +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 == "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( + 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 368f9a8..6fe05ca 100644 --- a/asr_worker.py +++ b/asr_worker.py @@ -1,7 +1,10 @@ import gc +import importlib.metadata import inspect import logging +import os import sys +import sysconfig import traceback from typing import Any @@ -51,6 +54,81 @@ 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 _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 @@ -78,6 +156,79 @@ 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) + 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), + ) + 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) + _prepare_sherpa_onnx_cuda_runtime() + + 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 + 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 @@ -148,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/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/build_release.ps1 b/build_release.ps1 index 1230973..514e189 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,21 @@ 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 "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 -& $Uv pip install --python $Py pysbd Write-Host "Setup complete." -ForegroundColor Green '@ diff --git a/config.yaml b/config.yaml index b1ca405..3be95c3 100644 --- a/config.yaml +++ b/config.yaml @@ -9,12 +9,41 @@ audio: chunk_duration: 0.032 asr: - # ASR engine: whisper, funasr, anime-whisper + # 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" # FunASR model: sensevoice-small, funasr-nano-2512, funasr-mlt-nano-2512 funasr_model: "sensevoice-small" + # 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 + 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: "" + # 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. + 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 @@ -27,6 +56,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) @@ -35,7 +71,7 @@ asr: translation: # OpenAI-compatible API settings api_base: "http://127.0.0.1:1234/v1" - api_key: "sk-lm-tHzDfNGm:dgxlip7eebn3HIMxivqN" + api_key: "dummy" model: "hunyuan-mt-chimera-7b" # Target language for translation target_language: "zh" diff --git a/control_panel.py b/control_panel.py index 707dad8..7718172 100644 --- a/control_panel.py +++ b/control_panel.py @@ -1,4 +1,3 @@ -import json import logging import os import threading @@ -8,6 +7,7 @@ from PyQt6.QtGui import QFont from PyQt6.QtWidgets import ( QApplication, + QCheckBox, QColorDialog, QComboBox, QDoubleSpinBox, @@ -42,41 +42,38 @@ 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, resolve_custom_whisper_model, ) 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): @@ -86,6 +83,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() @@ -98,74 +96,13 @@ 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 - ), - "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 = normalize_settings(config, saved) 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")) @@ -187,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 @@ -201,21 +138,19 @@ 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)", - "Remote Whisper (remote GPU server)", - ] - ) - engine_map_idx = { - "whisper": 0, - "funasr": 1, - "anime-whisper": 2, - "remote-whisper": 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"), + ("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")) + 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) @@ -232,6 +167,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"] @@ -271,6 +209,136 @@ 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_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_runtime_setting_changed + ) + 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_runtime_setting_changed + ) + 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() + 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_runtime_setting_changed + ) + 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_runtime_setting_changed + ) + 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._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) @@ -284,8 +352,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, 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")) @@ -301,8 +369,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, 15, 0) + asr_layout.addWidget(self._sensevoice_pad_seconds, 15, 1) self._sensevoice_pad_seconds.valueChanged.connect(self._auto_save) self._audio_device = QComboBox() @@ -324,8 +392,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")), 16, 0) + asr_layout.addWidget(self._audio_device, 16, 1) self._audio_device.currentIndexChanged.connect(self._auto_save) self._mic_device = QComboBox() @@ -346,16 +414,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")), 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")), 8, 0) - asr_layout.addWidget(self._hub_combo, 8, 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() @@ -364,8 +432,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")), 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) @@ -389,12 +457,94 @@ 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._asr_engine.currentIndexChanged.connect( - self._on_engine_changed_whisper_vis + self._whisper_group.setVisible(self._selected_asr_engine() == "whisper") + + 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", "") + 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) + layout.addWidget(self._crispasr_group) + 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._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" ) - self._on_engine_changed_whisper_vis(engine_idx) - self._update_whisper_size_label() # Remote ASR server URL — only visible when engine is Remote Whisper self._remote_group = QGroupBox("Remote ASR Server") @@ -407,21 +557,51 @@ def _create_vad_tab(self): self._remote_url_edit.editingFinished.connect(self._auto_save) remote_layout.addWidget(self._remote_url_edit, 1) layout.addWidget(self._remote_group) - self._remote_group.setVisible(engine_idx == 3) + self._remote_group.setVisible( + self._selected_asr_engine() == "remote-whisper" + ) + + 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() + 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() - 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) @@ -433,10 +613,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) @@ -448,7 +667,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) @@ -494,8 +713,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)) @@ -517,6 +734,8 @@ def _create_vad_tab(self): layout.addWidget(timing_group) + self._update_firered_vad_status() + self._update_vad_detail_visibility() layout.addStretch() return widget @@ -1094,7 +1313,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}") @@ -1104,14 +1327,56 @@ 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): - self._whisper_group.setVisible(index == 0) - is_funasr = index == 1 + 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" + + def _on_asr_engine_changed(self, index): + engine = self._selected_asr_engine() + is_whisper = engine == "whisper" + is_funasr = engine == "funasr" + 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) + 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) + 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, "_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, "_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"): - is_whisper = index == 0 self._whisper_pad_label.setVisible(is_whisper) self._whisper_pad_seconds.setVisible(is_whisper) if hasattr(self, "_sensevoice_pad_seconds"): @@ -1121,7 +1386,7 @@ def _on_engine_changed_whisper_vis(self, index): self._sensevoice_pad_label.setVisible(show_funasr_pad) self._sensevoice_pad_seconds.setVisible(show_funasr_pad) if hasattr(self, "_remote_group"): - self._remote_group.setVisible(index == 3) + self._remote_group.setVisible(is_remote) # Resize window to fit content after whisper group visibility change def _fit(): self.adjustSize() @@ -1135,13 +1400,453 @@ 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() + 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()] + + def _selected_crispasr_punc_model(self) -> str: + value = self._crispasr_punc_model.currentData() + return str(value) if value else "auto" + + 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() + ) + 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._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_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 "" + + 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"), "") + + 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_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"), "") + + 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 not saved_value: + selected = "" + 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: @@ -1228,6 +1933,23 @@ 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 + + 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;") + return + hub = self._current_settings.get("hub", "ms") + cached = is_asr_cached("crispasr", model_key, hub) + 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;") + # ── Model Management ── def _refresh_model_list(self): @@ -1247,6 +1969,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) @@ -1353,8 +2128,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 @@ -1434,15 +2220,17 @@ 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: "remote-whisper", - } - self._current_settings["asr_engine"] = engine_map.get( - self._asr_engine.currentIndex(), "whisper" + 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"): url = self._remote_url_edit.text().strip() @@ -1451,6 +2239,47 @@ 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() + ) + 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() + ) + 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() @@ -1497,7 +2326,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/i18n/CHANGELOG_en.md b/i18n/CHANGELOG_en.md index fa5517b..8a9ff6a 100644 --- a/i18n/CHANGELOG_en.md +++ b/i18n/CHANGELOG_en.md @@ -1,11 +1,22 @@ # 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 - New "Remote Whisper" ASR engine: offload speech recognition to a separate GPU machine (ships `asr_server.py` server), so a box without a GPU can still transcribe in real time - New "WebID / ID Verify" translation prompt preset, tuned for video identity-verification calls -- ASR now runs in an isolated subprocess: the worker auto-restarts on crash/timeout and recycles when memory grows past a threshold, so a recognition failure no longer drags down the UI +- ASR now runs in an isolated subprocess, so recognition failures no longer drag down the UI process - Subtitle window mouse click-through (#28): a toggle in the subtitle settings plus a "Subtitle Click-through" tray shortcut; when on, clicks pass to the window behind (middle-click drag is disabled while on — turn it off to reposition) +## 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 7b5026d..d841d2b 100644 --- a/i18n/CHANGELOG_zh.md +++ b/i18n/CHANGELOG_zh.md @@ -1,11 +1,22 @@ # 更新日志 ## 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 - 新增「远程 Whisper」ASR 引擎: 把语音识别外包到带 GPU 的另一台机器 (附 `asr_server.py` 服务端), 无 GPU 的机器也能实时识别 - 新增「WebID 身份核验」翻译提示词预设: 针对视频核验场景调优用词 -- ASR 改为子进程隔离运行: worker 崩溃/超时自动重启, 内存增长到阈值自动回收, 识别故障不再拖垮界面 +- ASR 改为子进程隔离运行: 识别故障不再拖垮界面进程 - 字幕窗口支持鼠标穿透 (#28): 字幕设置里新增开关, 托盘菜单可「字幕鼠标穿透」快速切换, 开启后点击直达背后窗口 (此时中键拖动失效, 先关穿透再移动) +## 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 04336f9..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" @@ -81,6 +83,20 @@ 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_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." @@ -98,10 +114,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:" @@ -153,6 +183,38 @@ whisper_local_prefix: "Local" whisper_local_ready: "Local model" whisper_missing_local: "Missing" whisper_invalid_local: "Local model unavailable" +group_crispasr_model: "CrispASR Model" +crispasr_model_placeholder: "Select CrispASR model" +crispasr_select_model: "Select a model" +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" +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 8683a3f..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: "缓存" @@ -81,6 +83,20 @@ 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_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 表示关闭。" @@ -98,10 +114,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: "最短语音:" @@ -153,6 +183,38 @@ whisper_local_prefix: "本地" whisper_local_ready: "本地模型" whisper_missing_local: "缺失" whisper_invalid_local: "本地模型不可用" +group_crispasr_model: "CrispASR 模型" +crispasr_model_placeholder: "请选择 CrispASR 模型" +crispasr_select_model: "请选择模型" +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: "本地模型不可用" +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 74eb4c7..9a78351 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,6 +1,27 @@ # 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", + + [switch]$DownloadFireRedVAD, + + [ValidateSet("ms", "hf")] + [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" $ProjectDir = Split-Path -Parent $MyInvocation.MyCommand.Path Set-Location $ProjectDir @@ -10,6 +31,210 @@ 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 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 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 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. @@ -51,6 +276,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 +338,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 +446,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 +455,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 +464,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 +516,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,34 +529,53 @@ 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" + +# 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: Verify FireRedVAD dependency ── +Test-FireRedVadPackage -PythonExe $Python -# ── Step 6: Install FunASR (no-deps) ── +if ($DownloadFireRedVAD) { + Download-FireRedVADModel -PythonExe $Python -Hub $FireRedVADHub +} + +# ── Step 7: Install CrispASR native runtime ── +Install-CrispAsrNativeRuntime -PythonExe $Python -UseCuda $HasNvidia + +# ── Step 8: Install sherpa-onnx runtime ── +Install-SherpaOnnxRuntime -PythonExe $Python -Runtime $SherpaOnnxRuntime + +# ── Step 9: 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..." +# ── Step 10: Optional parakeet.cpp runtime/model ── +if ($InstallParakeetCpp) { + Install-ParakeetCppRuntime -PythonExe $Python -Backend $ParakeetCppBackend -Version $ParakeetCppVersion +} -& $Pip install pysbd -if ($LASTEXITCODE -ne 0) { - Write-Warn "pysbd installation failed (incremental ASR may not work)" -} else { - Write-Ok "pysbd installed" +if ($DownloadParakeetCppModel) { + Download-ParakeetCppModel -PythonExe $Python -ModelName $ParakeetCppModel } # ── Done ── diff --git a/main.py b/main.py index 53fa558..e2f651e 100644 --- a/main.py +++ b/main.py @@ -7,29 +7,19 @@ 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 from model_manager import ( DEFAULT_FUNASR_MODEL, apply_cache_env, - funasr_display_name, - funasr_supports_padding, get_missing_models, - is_asr_cached, - ASR_DISPLAY_NAMES, - MODELS_DIR, - local_faster_whisper_display_name, migrate_funasr_settings, normalize_asr_engine_selection, - normalize_funasr_model_key, - resolve_custom_whisper_model, ) # Set cache env BEFORE importing torch so TORCH_HOME is respected @@ -40,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_client import ASRClient, ASRWorkerError, ASRWorkerExited, ASRWorkerTimeout +from asr_service import ASRService +from pipeline_controller import PipelineController from translator import Translator, RepetitionError from transcript_writer import TranscriptWriter @@ -62,12 +51,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, @@ -75,8 +60,6 @@ ) from i18n import t, set_lang, LANGUAGES, COMMON_LANG_CODES -_NO_PENDING = object() - def setup_logging(): log_dir = Path(__file__).parent / "logs" @@ -160,53 +143,20 @@ 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"), - sample_rate=config["audio"]["sample_rate"], - 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 = 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._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._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_lock = threading.RLock() - self._vad_lock = threading.Lock() - # Settings changed from the Qt thread are deferred here and applied by the - # ASR thread before its next transcribe, so the UI never blocks on the - # worker pipe (which may be busy with an in-flight cross-process call). - # Padding is keyed by engine_type because one settings save updates both - # the funasr and whisper padding and they must not clobber each other. - self._asr_pending_lock = threading.Lock() - self._asr_pending_language = _NO_PENDING - self._asr_pending_padding = {} - # Auto-restart bookkeeping for a worker that dies mid-session. _asr_generation - # is bumped on every (de)activation so a slow background (re)start can detect - # that a newer engine switch superseded it and discard its stale worker. - self._asr_restart_state = None - self._asr_restart_count = 0 - self._asr_restart_max = 3 - self._asr_generation = 0 - self._asr_recycling = False - # Proactively recycle the worker once its RSS grows this far past the - # post-load baseline, to bound native-side (FunASR/CTranslate2) leaks that - # accumulate in the long-lived worker process. - self._asr_worker_baseline_mb = None - self._asr_recycle_delta_mb = 2048 self._target_language = config["translation"]["target_language"] self._translator = Translator( api_base=config["translation"]["api_base"], @@ -224,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") @@ -256,15 +203,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 @@ -281,16 +219,30 @@ 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: - 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 ( @@ -298,29 +250,32 @@ 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", + "sherpa_onnx_model", + "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", ) ): 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: - 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: @@ -330,135 +285,17 @@ 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 - self._asr_restart_state = None - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - 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 - self._asr_restart_state = None - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - 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_pending_lock: - self._asr_pending_language = language - - def _set_asr_padding(self, engine_type: str, pad_seconds): - with self._asr_pending_lock: - self._asr_pending_padding[engine_type] = pad_seconds - - def _apply_pending_asr_settings(self, client, asr_type, funasr_key): - """Apply deferred language/padding on the ASR thread, just before a transcribe. - A pending value is cleared only once delivered; worker-death exceptions - propagate with the pending intact so the restarted worker re-applies it. The - applied value is written back into the restart config so an auto-restart or - recycle does not revert a runtime override to the engine-switch-time value.""" - with self._asr_pending_lock: - language = self._asr_pending_language - pad_seconds = self._asr_pending_padding.get(asr_type, _NO_PENDING) - if language is not _NO_PENDING: - try: - client.set_language(language) - except ASRWorkerError as exc: - log.warning(f"ASR language update failed: {exc}") - self._update_restart_config(language=language) - self._clear_pending_language(language) - if pad_seconds is not _NO_PENDING: - if not (asr_type == "funasr" and not funasr_supports_padding(funasr_key)): - try: - client.set_input_padding(pad_seconds) - except ASRWorkerError as exc: - log.warning(f"ASR padding update failed: {exc}") - self._update_restart_config(pad_seconds=pad_seconds) - self._clear_pending_padding(asr_type, pad_seconds) - - def _clear_pending_language(self, language): - with self._asr_pending_lock: - if self._asr_pending_language is language: - self._asr_pending_language = _NO_PENDING - - def _clear_pending_padding(self, asr_type, pad_seconds): - with self._asr_pending_lock: - if self._asr_pending_padding.get(asr_type) == pad_seconds: - del self._asr_pending_padding[asr_type] - - def _update_restart_config(self, **kwargs): - with self._asr_lock: - if self._asr_restart_state and self._asr_restart_state.get("config"): - self._asr_restart_state["config"].update(kwargs) - - def _load_engine_client(self, config: dict): - """Build the ASR backend for a worker config. Local engines run in an isolated - worker subprocess (ASRClient); remote-whisper is a thin in-process HTTP client - that needs no subprocess isolation (no native deps, no GPU model to load).""" - if config.get("engine_type") == "remote-whisper": - from asr_remote import RemoteASREngine - - url = config.get("remote_asr_url") or "http://127.0.0.1:8765" - engine = RemoteASREngine(server_url=url) - language = config.get("language") - if language: - engine.set_language(language) - return engine - return self._load_asr_client(config) - - def _load_asr_client(self, worker_config: dict) -> ASRClient: - # request_timeout bounds how long a hung worker can stall the realtime path - # before it is killed and auto-restarted. VAD caps segments at a few seconds, - # so 60s is generous for a healthy transcribe yet far below the old 120s. - client = ASRClient(worker_config, request_timeout=60.0) - 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}") 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( @@ -498,204 +335,45 @@ 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 - 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 - - remote_asr_url = settings.get( - "remote_asr_url", - self._config["asr"].get("remote_asr_url", "http://127.0.0.1:8765"), - ) - - 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 == "remote-whisper": - # URL is part of the identity so editing it triggers a reconnect. - signature_model = remote_asr_url - else: - signature_model = engine_type - signature = (engine_type, signature_model, device, hub, compute) - - 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: + 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 - 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 - 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() - - 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) + self._pipeline.reset_for_asr_switch() 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": settings.get( - "asr_language", self._config["asr"].get("language", "auto") - ), - "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, - "remote_asr_url": remote_asr_url, - } - 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, - "config": worker_config, - "display_name": display_name, - "device_label": ( - remote_asr_url if engine_type == "remote-whisper" else device - ), - } - - 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: {engine_type}") - with self._asr_lock: - self._asr_ready = ( - self._asr is not None and self._asr.status == "ready" - ) - return - - 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, - "config": old_config, - "display_name": (old_config or {}).get("display_name"), - "device_label": ( - (old_config or {}).get("remote_asr_url") - if self._asr_type == "remote-whisper" - else self._asr_device - ), - } - self._asr = None - self._asr_ready = False - self._asr_type = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 - self._asr_restart_state = None - self._asr_worker_baseline_mb = None - self._asr_generation += 1 + if plan.missing_models: + dlg = ModelDownloadDialog( + plan.missing_models, + hub=plan.hub, + proxy=plan.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 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_engine_client(worker_config) - except Exception as e: - load_error[0] = str(e) - # A remote server that is simply down is an expected, user-actionable - # condition, not a bug, so skip the noisy traceback for it. - expected = isinstance(e, ConnectionError) - log.error( - f"Failed to load ASR worker: {e}", exc_info=not expected - ) - if old_config: - try: - log.info("Restoring previous ASR worker after switch failure") - restored_asr[0] = self._load_engine_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() @@ -714,33 +392,21 @@ 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._asr_ready = True - self._asr_error_count = 0 - self._asr_restart_state = dict(state) - self._asr_restart_count = 0 - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - - 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": + target_state = result.target_state or plan.target_state or {} + device_label = target_state.get("device_label", plan.device) if self._overlay: self._overlay.update_asr_device( - f"{display_name} [{target_state['device_label']}]" + f"{plan.display_name} [{device_label}]" ) - log.info(f"ASR worker ready: {engine_type} on {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( @@ -751,22 +417,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, @@ -782,13 +444,13 @@ def _mem_snapshot(self) -> dict: # The ASR model (and its native-side leak) lives in the worker process now, # so sample its RSS too; the main process holds only VAD + Qt. worker_rss_mb = 0.0 - client = self._asr - if client is not None and client.pid is not None: + worker_pid = self._asr_service.worker_pid + if worker_pid is not None: try: import psutil worker_rss_mb = ( - psutil.Process(client.pid).memory_info().rss / 1024 / 1024 + psutil.Process(worker_pid).memory_info().rss / 1024 / 1024 ) except Exception: worker_rss_mb = 0.0 @@ -801,7 +463,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._pipeline.buffer_stats()["chunks"] return { "rss": rss_mb, "worker_rss": worker_rss_mb, @@ -836,210 +498,23 @@ 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() - # Snapshot the active client under the lock, then release it: the blocking - # cross-process transcribe must not hold _asr_lock, or a slow/hung worker - # would freeze the Qt thread on every settings change. ASRClient serializes - # its own pipe access, and only this (single) ASR thread calls transcribe. - with self._asr_lock: - if not self._asr_ready or self._asr is None: - return None, 0.0 - client = self._asr - asr_type = self._asr_type - funasr_key = self._funasr_model_key + if not self._asr_service.is_ready: + return None, 0.0 try: - self._apply_pending_asr_settings(client, asr_type, funasr_key) - 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._recover_asr_worker(client, str(exc)) - 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) - fatal = False - with self._asr_lock: - if self._asr is client: - self._asr_error_count += 1 - fatal = not exc.recoverable or self._asr_error_count >= 3 - if fatal: - self._mark_asr_unavailable(str(exc), client) - raise + 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 - with self._asr_lock: - if self._asr is client: - self._asr_error_count = 0 - self._asr_restart_count = 0 + 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 - def _start_worker_from_state(self, state: dict, expected_gen: int) -> bool: - """Load a worker from a saved state and activate it only if no newer engine - switch happened in the meantime (generation guard). Runs on the ASR thread; - the load is intentionally done outside _asr_lock. Returns True on activation.""" - try: - client = self._load_engine_client(state["config"]) - except Exception as e: - log.error(f"ASR worker (re)start failed: {e}", exc_info=True) - return False - stale = None - with self._asr_lock: - if self._asr_generation != expected_gen or not self._running: - stale = client - else: - 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._asr_ready = True - self._asr_error_count = 0 - self._asr_restart_state = dict(state) - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - if stale is not None: - log.info("Discarding superseded ASR worker (newer switch won the race)") - try: - stale.shutdown() - except Exception: - pass - return False - name = state.get("display_name") or state.get("type") - if self._overlay: - self._overlay.update_asr_device( - f"{name} [{state.get('device_label', state['device'])}]" - ) - return True - - def _recover_asr_worker(self, dead_client, reason: str): - """Auto-restart a worker that died mid-session. Without this, a single crash - or transcribe timeout would leave ASR permanently silent for the session.""" - with self._asr_lock: - if self._asr is not dead_client: - return # an engine switch already replaced/cleared it - state = dict(self._asr_restart_state) if self._asr_restart_state else None - attempt = self._asr_restart_count + 1 - give_up = ( - state is None - or not state.get("config") - or attempt > self._asr_restart_max - ) - self._asr_restart_count = attempt - self._asr = None - self._asr_ready = False - self._asr_type = None - self._asr_signature = None - self._asr_config = None - self._asr_error_count = 0 - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - gen = self._asr_generation - try: - dead_client.shutdown() - except Exception: - try: - dead_client.terminate() - except Exception: - pass - if not self._running: - return # shutting down; do not spawn a replacement worker - if give_up: - log.error( - f"ASR worker died and auto-restart gave up after " - f"{self._asr_restart_max} attempts: {reason}" - ) - if self._overlay: - self._overlay.update_asr_device("ASR unavailable") - return - log.warning( - f"ASR worker died ({reason}); auto-restart attempt " - f"{attempt}/{self._asr_restart_max}" - ) - self._release_memory_caches() - if self._start_worker_from_state(state, gen): - log.info( - f"ASR worker auto-restarted: {state.get('type')} on " - f"{state.get('device')}" - ) - elif self._asr is None and self._overlay: - self._overlay.update_asr_device("ASR unavailable") - - def _maybe_recycle_asr_worker(self): - """Recycle the worker once its RSS grows well past the post-load baseline, to - bound native-side leaks that accumulate in the long-lived worker process. - Called from the ASR thread between segments so the reload gap costs no audio - beyond what arrives during it.""" - if not self._running: - return - with self._asr_lock: - client = self._asr - if not self._asr_ready or client is None or self._asr_recycling: - return - state = dict(self._asr_restart_state) if self._asr_restart_state else None - if state is None or not state.get("config") or client.pid is None: - return - try: - import psutil - - rss = psutil.Process(client.pid).memory_info().rss / 1024 / 1024 - except Exception: - return - if self._asr_worker_baseline_mb is None: - self._asr_worker_baseline_mb = rss - return - if rss < self._asr_worker_baseline_mb + self._asr_recycle_delta_mb: - return - log.warning( - f"ASR worker RSS={rss:.0f}MB grew " - f"{rss - self._asr_worker_baseline_mb:.0f}MB over baseline; recycling" - ) - self._recycle_asr_worker(client, state) - - def _recycle_asr_worker(self, old_client, state: dict): - # Graceful stop-then-start (no VRAM doubling). The generation guard makes a - # concurrent engine switch win over this recycle. - with self._asr_lock: - if self._asr is not old_client: - return - self._asr = None - self._asr_ready = False - self._asr_recycling = True - self._asr_worker_baseline_mb = None - self._asr_generation += 1 - gen = self._asr_generation - try: - old_client.shutdown() - except Exception: - try: - old_client.terminate() - except Exception: - pass - self._release_memory_caches() - if not self._running: - with self._asr_lock: - self._asr_recycling = False - return - try: - started = self._start_worker_from_state(state, gen) - finally: - with self._asr_lock: - self._asr_recycling = False - if started: - log.info(f"ASR worker recycled: {state.get('type')} on {state.get('device')}") - else: - log.error("ASR worker recycle failed to restart") - if self._asr is None and self._overlay: - self._overlay.update_asr_device("ASR unavailable") - def _check_memory_threshold(self, rss_mb: float): if self._mem_warned or rss_mb < self._mem_threshold_mb: return @@ -1167,18 +642,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() @@ -1190,34 +656,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_ready: - self._process_interim_final(remaining) - else: - remaining = self._vad.flush() - if remaining is not None and self._asr_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: @@ -1233,71 +675,21 @@ 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() - log.info("Pipeline stopped") + self._asr_service.shutdown() 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( @@ -1305,17 +697,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: @@ -1331,408 +721,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._get_effective_silence_limit() + 1 - for _ in range(n): - with self._vad_lock: - seg = self._vad.process_chunk(silence_chunk) - if seg is not None and self._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))) - - 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_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_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: - # Idle moment: recycle a bloated worker while no audio is waiting. - # Guarded so an unexpected error can never kill this thread (which - # would itself silence ASR permanently). - try: - self._maybe_recycle_asr_worker() - except Exception: - log.error("ASR worker recycle check failed", exc_info=True) - 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() log.info("LiveTranslate starting...") @@ -1740,7 +754,16 @@ def main(): config.setdefault("asr", {}) config["asr"].setdefault("asr_engine", "funasr") config["asr"].setdefault("funasr_model", DEFAULT_FUNASR_MODEL) - saved = _load_saved_settings() + 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") + 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) # Log actual effective config @@ -1784,7 +807,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 @@ -1800,7 +823,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")) @@ -1809,7 +832,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 @@ -1817,13 +840,31 @@ 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", ""), + ) + elif current_engine == "sherpa-onnx": + startup_model_key = saved.get( + "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( 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: @@ -1955,33 +996,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] @@ -2033,7 +1067,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) @@ -2106,24 +1140,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) @@ -2155,27 +1181,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) @@ -2209,12 +1220,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): @@ -2250,19 +1255,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) + live_trans._asr_service.set_language(code) + 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() @@ -2298,15 +1295,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._asr_service.set_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/model_manager.py b/model_manager.py index 95ea8d7..12948b9 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,9 @@ 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", @@ -139,11 +143,38 @@ def asr_model_id( "funasr-mlt-nano": "Fun-ASR-MLT-Nano", "whisper": "Whisper", "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, + "firered-vad": 2_200_000, "sensevoice": 940_000_000, "funasr-nano": 1_050_000_000, "funasr-mlt-nano": 1_050_000_000, @@ -221,6 +252,319 @@ 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 + path = Path(path) + try: + return ( + 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 + + +def _custom_crispasr_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 resolve_custom_crispasr_model(value) -> str | None: + path = _custom_crispasr_path(value) + if path and is_crispasr_model_file(path): + return str(path.absolute()) + return None + + def _custom_whisper_path(value) -> Path | None: if not value or value in _WHISPER_SIZES: return None @@ -254,6 +598,12 @@ 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_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": @@ -315,6 +665,532 @@ 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 path.name == "model.bin" and is_faster_whisper_model_dir(path.parent): + continue + if not is_crispasr_model_file(path): + continue + try: + identity = str(path.resolve()) + model_path = str(path.absolute()) + except OSError: + continue + if identity in seen: + continue + 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": model_path}) + + 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 _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", + "nemo_ctc": "nemo_ctc", + "nemo-ctc": "nemo_ctc", + "whisper": "whisper", + "online_transducer": "online_transducer", + "online-transducer": "online_transducer", + } + return aliases.get(family) + 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 _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: + 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" + 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) + + 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.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} + 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 _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()) @@ -380,6 +1256,12 @@ 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 == "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] @@ -441,6 +1323,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 in ("crispasr", "sherpa-onnx", "parakeet-cpp"): + return missing if engine == "funasr" or engine in FUNASR_LEGACY_ENGINE_ALIASES: model_key = ( FUNASR_LEGACY_ENGINE_ALIASES[engine] @@ -469,11 +1353,26 @@ 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. """ + 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 == "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 = ( FUNASR_LEGACY_ENGINE_ALIASES[engine_type] @@ -585,9 +1484,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 @@ -603,33 +1499,21 @@ 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}") -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(): + 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): @@ -677,6 +1561,21 @@ 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"]))) + + 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"]))) + if torch_base.exists(): for d in sorted(torch_base.glob("snakers4_silero-vad*")): if d.is_dir(): 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()) + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ff53dde --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[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", + "fireredvad>=0.0.2,<0.1", + "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/settings_store.py b/settings_store.py new file mode 100644 index 0000000..0404218 --- /dev/null +++ b/settings_store.py @@ -0,0 +1,188 @@ +import json +import logging +from pathlib import Path +from typing import Any + +from model_manager import ( + DEFAULT_FIRERED_VAD_MODEL, + DEFAULT_FUNASR_MODEL, + DEFAULT_PARAKEET_CPP_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( + "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") + 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("sherpa_onnx_model", asr.get("sherpa_onnx_model", "")) + settings.setdefault( + "remote_asr_url", asr.get("remote_asr_url", "http://127.0.0.1:8765") + ) + 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( + "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) + ) + 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/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..5145fe0 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2157 @@ +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 = "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" +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 = "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" +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 = "fireredvad" }, + { 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 = "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" }, + { 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 = "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" +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" }, +] 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 b0f23ed..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 @@ -76,6 +83,29 @@ 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() + self._reset_firered_adapter() + + 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)) @@ -95,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 @@ -115,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} " @@ -134,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 @@ -157,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: @@ -256,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}: " @@ -319,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 )