diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..676dda42
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+__pycache__/
+*.pyc
+*.pyo
+*.egg-info/
+*.egg
+dist/
+build/
+.venv/
+.env
+.DS_Store
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+*.so
+/.cache*
+/exp*/
+/.tmp/
+/results/
+/data/
+/download
+/local/
+/run*
+example.py
+results/
+examples/data*
+examples/download*
+examples/exp*/
+.claude/
+*.wav
+*.jsonl
+
+# Generated logs and binaries
+*.log
+cloudflared
+cloudflared.tgz
+
+# Data directories and sqlite db
+omnivoice_data/
+*.db
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 9b76c26a..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "OmniVoice"]
- path = OmniVoice
- url = https://github.com/k2-fsa/OmniVoice
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..6a14dc52
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2026 Xiaomi Corp.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/OmniVoice b/OmniVoice
deleted file mode 160000
index 77c284d9..00000000
--- a/OmniVoice
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 77c284d9e7354d3c29592635955a417234fb98ac
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..cf70064f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,301 @@
+# OmniVoice 🌍
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+OmniVoice is a state-of-the-art massively multilingual zero-shot text-to-speech (TTS) model supporting over 600 languages. Built on a novel diffusion language model-style architecture, it generates high-quality speech with superior inference speed, supporting voice cloning and voice design.
+
+**Contents**: [Key Features](#key-features) | [Installation](#installation) | [Quick Start](#quick-start) | [Python API](#python-api) | [Command-Line Tools](#command-line-tools) | [Training & Evaluation](#training--evaluation) | [Discussion](#discussion--communication) | [Citation](#citation)
+
+## Key Features
+
+- **600+ Languages Supported**: The broadest language coverage among zero-shot TTS models ([full list](docs/languages.md)).
+- **Voice Cloning**: State-of-the-art voice cloning quality.
+- **Voice Design**: Control voices via assigned speaker attributes (gender, age, pitch, dialect/accent, whisper, etc.).
+- **Fine-grained Control**: Non-verbal symbols (e.g., `[laughter]`) and pronunciation correction via pinyin or phonemes.
+- **Fast Inference**: RTF as low as 0.025 (40x faster than real-time).
+- **Diffusion Language Model-style Architecture**: A clean, streamlined, and scalable design that delivers both quality and speed.
+
+---
+
+## Installation
+
+Choose **one** of the following methods: **pip** or **uv**.
+
+### pip
+
+> We recommend using a fresh virtual environment (e.g., `conda`, `venv`, etc.) to avoid conflicts.
+
+**Step 1**: Install PyTorch
+
+
+NVIDIA GPU
+
+```bash
+# Install pytorch with your CUDA version, e.g.
+pip install torch==2.8.0+cu128 torchaudio==2.8.0+cu128 --extra-index-url https://download.pytorch.org/whl/cu128
+```
+> See [PyTorch official site](https://pytorch.org/get-started/locally/) for other versions installation.
+
+
+
+
+Apple Silicon
+
+```bash
+pip install torch==2.8.0 torchaudio==2.8.0
+```
+
+
+
+**Step 2**: Install OmniVoice (choose one)
+
+```bash
+# From PyPI (stable release)
+pip install omnivoice
+
+# From the latest source on GitHub (no need to clone)
+pip install git+https://github.com/k2-fsa/OmniVoice.git
+
+# For development (clone first, editable install)
+git clone https://github.com/k2-fsa/OmniVoice.git
+cd OmniVoice
+pip install -e .
+```
+
+### uv
+
+Clone the repository and sync dependencies:
+
+```bash
+git clone https://github.com/k2-fsa/OmniVoice.git
+cd OmniVoice
+uv sync
+```
+
+> **Tip**: Can use mirror with `uv sync --default-index "https://mirrors.aliyun.com/pypi/simple"`
+
+---
+
+## Quick Start
+
+Try OmniVoice without coding:
+
+- Launch the local web UI: `omnivoice-demo --ip 0.0.0.0 --port 8001`
+
+
+- Or try it directly on [HuggingFace Space](https://huggingface.co/spaces/k2-fsa/OmniVoice)
+
+> If you have trouble connecting to HuggingFace when downloading the pre-trained models, set `export HF_ENDPOINT="https://hf-mirror.com"` before running.
+
+For full usage, see the [Python API](#python-api) and [Command-Line Tools](#command-line-tools) sections below.
+
+---
+
+## Python API
+
+OmniVoice supports three generation modes. All features in this section are also available via [command-line tools](#command-line-tools).
+
+### Voice Cloning
+
+Clone a voice from a short reference audio. Provide `ref_audio` and `ref_text`:
+
+```python
+from omnivoice import OmniVoice
+import torch
+import torchaudio
+
+model = OmniVoice.from_pretrained(
+ "k2-fsa/OmniVoice",
+ device_map="cuda:0",
+ dtype=torch.float16
+)
+# Apple Silicon users: use device_map="mps" instead
+
+audio = model.generate(
+ text="Hello, this is a test of zero-shot voice cloning.",
+ ref_audio="ref.wav",
+ ref_text="Transcription of the reference audio.",
+) # audio is a list of `torch.Tensor` with shape (1, T) at 24 kHz.
+
+# If you don't want to input `ref_text` manually, you can directly omit the `ref_text`.
+# The model will use Whisper ASR to auto-transcribe it.
+
+torchaudio.save("out.wav", audio[0], 24000)
+```
+
+> **Tips**
+>
+> - Use a 3–10 seconds reference audio clip. Longer audio slows down inference and may degrade cloning quality.
+> - For better results with Arabic numerals, normalize them to words first (e.g., "123" → "one hundred twenty-three") with text normalization tools (e.g., [WeTextProcessing](https://github.com/wenet-e2e/WeTextProcessing)).
+
+### Voice Design
+
+Describe the desired voice with speaker attributes — no reference audio needed.
+Supported attributes: **gender** (male/female), **age** (child to elderly),
+**pitch** (very low to very high), **style** (whisper), **English accent**
+(American, British, etc.), and **Chinese dialect** (四川话, 陕西话, etc.).
+Attributes are comma-separated and freely combinable across categories.
+
+```python
+audio = model.generate(
+ text="Hello, this is a test of zero-shot voice design.",
+ instruct="female, low pitch, british accent",
+)
+```
+
+See [docs/voice-design.md](docs/voice-design.md) for the full attribute
+reference, Chinese equivalents, and usage tips.
+
+### Auto Voice
+
+Let the model choose a voice automatically:
+
+```python
+audio = model.generate(text="This is a sentence without any voice prompt.")
+```
+
+### Generation Parameters
+
+All above three modes share the same `model.generate()` API. You can further control the generation behavior via keyword arguments:
+
+```python
+audio = model.generate(
+ text="...",
+ num_step=32, # diffusion steps (or 16 for faster inference)
+ speed=1.0, # speed factor (>1.0 faster, <1.0 slower)
+ duration=10.0, # fixed output duration in seconds (overrides speed)
+ # ... more options
+)
+```
+See more detailed control in [docs/generation-parameters.md](docs/generation-parameters.md).
+
+### Non-Verbal & Pronunciation Control
+
+OmniVoice supports inline **non-verbal symbols** and **pronunciation correction** within the input text.
+
+**Non-verbal symbols**: Insert tags like `[laughter]` directly in the text to add expressive non-verbal sounds.
+
+```python
+audio = model.generate(text="[laughter] You really got me. I didn't see that coming at all.")
+```
+
+Supported tags: `[laughter]`, `[sigh]`, `[confirmation-en]`, `[question-en]`, `[question-ah]`, `[question-oh]`, `[question-ei]`, `[question-yi]`, `[surprise-ah]`, `[surprise-oh]`, `[surprise-wa]`, `[surprise-yo]`, `[dissatisfaction-hnn]`.
+
+**Pronunciation control (Chinese)**: Use pinyin with tone numbers to correct specific character pronunciations.
+
+```python
+audio = model.generate(text="这批货物打ZHE2出售后他严重SHE2本了,再也经不起ZHE1腾了。")
+```
+
+**Pronunciation control (English)**: Use [CMU pronunciation dictionary](https://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict.0.7a) (uppercase, in brackets) to override default English pronunciations.
+
+```python
+audio = model.generate(text="He plays the [B EY1 S] guitar while catching a [B AE1 S] fish.")
+```
+
+---
+
+## Command-Line Tools
+
+Three CLI entry points are provided. The CLI tools support all features available in the Python API (voice cloning, voice design, auto voice, generation parameters, etc.) — all controlled via command-line arguments.
+
+| Command | Description | Source |
+|---|---|---|
+| `omnivoice-demo` | Interactive Gradio web demo | [omnivoice/cli/demo.py](omnivoice/cli/demo.py) |
+| `omnivoice-infer` | Single-item inference | [omnivoice/cli/infer.py](omnivoice/cli/infer.py) |
+| `omnivoice-infer-batch` | Batch inference across multiple GPUs | [omnivoice/cli/infer_batch.py](omnivoice/cli/infer_batch.py) |
+
+### Demo
+
+```bash
+omnivoice-demo --ip 0.0.0.0 --port 8001
+```
+
+Provides a web UI for voice cloning and voice design. See `omnivoice-demo --help` for all options.
+
+### Single Inference
+
+```bash
+# Voice Cloning
+# ref_text can be omitted (Whisper will auto-transcribe ref_audio to get it).
+omnivoice-infer \
+ --model k2-fsa/OmniVoice \
+ --text "This is a test for text to speech." \
+ --ref_audio ref.wav \
+ --ref_text "Transcription of the reference audio." \
+ --output hello.wav
+
+# Voice Design
+omnivoice-infer --model k2-fsa/OmniVoice \
+ --text "This is a test for text to speech." \
+ --instruct "male, British accent" \
+ --output hello.wav
+
+# Auto Voice
+omnivoice-infer \
+ --model k2-fsa/OmniVoice \
+ --text "This is a test for text to speech."\
+ --output hello.wav
+```
+
+### Batch Inference
+
+`omnivoice-infer-batch` can distribute batch inference across multiple GPUs, designed for large-scale TTS tasks.
+
+```bash
+omnivoice-infer-batch \
+ --model k2-fsa/OmniVoice \
+ --test_list test.jsonl \
+ --res_dir results/
+```
+
+The test list is a JSONL file where each line is a JSON object:
+```json
+{"id": "sample_001", "text": "Hello world", "ref_audio": "/path/to/ref.wav", "ref_text": "Reference transcript", "instruct": "female, british accent", "language_id": "en", "language_name": "English", "duration": 10.0, "speed": 1.0}
+```
+Only `id` and `text` are mandatory fields. `ref_audio` and `ref_text` are used in voice cloning mode. `instruct` is used in voice design mode. If no reference audio or instruct are provided, the model will generate text in a random voice.
+
+`language_id`, `language_name`, `duration`, and `speed` are optional. `duration` (in seconds) fixes the output length; `speed` controls the speaking rate. If `duration` and `speed` are both provided, `speed` will be ignored.
+
+---
+
+## Training & Evaluation
+
+See [examples/](examples/) for the complete pipeline — from data preparation to training, evaluation, and finetuning.
+
+---
+
+## Discussion & Communication
+
+You can directly discuss on [GitHub Issues](https://github.com/k2-fsa/OmniVoice/issues).
+
+You can also scan the QR code to join our wechat group or follow our wechat official account.
+
+| Wechat Group | Wechat Official Account |
+| ------------ | ----------------------- |
+| | |
+
+---
+
+## Citation
+
+```bibtex
+@article{zhu2026omnivoice,
+ title={OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models},
+ author={Zhu, Han and Ye, Lingxuan and Kang, Wei and Yao, Zengwei and Guo, Liyong and Kuang, Fangjun and Han, Zhifeng and Zhuang, Weiji and Lin, Long and Povey, Daniel},
+ journal={arXiv preprint arXiv:2604.00688},
+ year={2026}
+}
+```
diff --git a/api.py b/api.py
new file mode 100644
index 00000000..dfbec4fc
--- /dev/null
+++ b/api.py
@@ -0,0 +1,919 @@
+import io
+import os
+import uuid
+import json
+import shutil
+import sqlite3
+import tempfile
+import asyncio
+import subprocess
+import logging
+import time
+from contextlib import asynccontextmanager
+from typing import Optional, List
+from concurrent.futures import ThreadPoolExecutor
+
+import numpy as np
+import soundfile as sf
+import torch
+import torchaudio
+from fastapi import FastAPI, File, Form, UploadFile, HTTPException, Query
+from fastapi.responses import FileResponse, Response, StreamingResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import BaseModel
+
+from omnivoice.models.omnivoice import OmniVoice
+
+logger = logging.getLogger("omnivoice.api")
+
+# ═══════════════════════════════════════════════════════════════════════
+# PATHS & GLOBALS
+# ═══════════════════════════════════════════════════════════════════════
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), "omnivoice_data")
+VOICES_DIR = os.path.join(DATA_DIR, "voices") # Reference audio for profiles
+OUTPUTS_DIR = os.path.join(DATA_DIR, "outputs") # Generated audio files
+DUB_DIR = os.path.join(DATA_DIR, "dub_jobs")
+DB_PATH = os.path.join(DATA_DIR, "omnivoice.db")
+
+for d in [DATA_DIR, VOICES_DIR, OUTPUTS_DIR, DUB_DIR]:
+ os.makedirs(d, exist_ok=True)
+
+# Ensure ffmpeg is on PATH for Whisper and other subprocesses
+for _fpath in ["/opt/homebrew/bin", "/usr/local/bin"]:
+ if _fpath not in os.environ.get("PATH", ""):
+ os.environ["PATH"] = _fpath + ":" + os.environ.get("PATH", "")
+
+model: Optional[OmniVoice] = None
+_inference_pool = ThreadPoolExecutor(max_workers=1)
+_dub_jobs = {}
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# SQLITE DATABASE
+# ═══════════════════════════════════════════════════════════════════════
+
+def _get_db():
+ conn = sqlite3.connect(DB_PATH)
+ conn.row_factory = sqlite3.Row
+ conn.execute("PRAGMA journal_mode=WAL")
+ return conn
+
+
+def _init_db():
+ conn = _get_db()
+ conn.executescript("""
+ CREATE TABLE IF NOT EXISTS voice_profiles (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ ref_audio_path TEXT,
+ ref_text TEXT DEFAULT '',
+ instruct TEXT DEFAULT '',
+ language TEXT DEFAULT 'Auto',
+ created_at REAL
+ );
+ CREATE TABLE IF NOT EXISTS generation_history (
+ id TEXT PRIMARY KEY,
+ text TEXT,
+ mode TEXT,
+ language TEXT,
+ instruct TEXT,
+ profile_id TEXT,
+ audio_path TEXT,
+ duration_seconds REAL,
+ generation_time REAL,
+ created_at REAL,
+ FOREIGN KEY (profile_id) REFERENCES voice_profiles(id)
+ );
+ CREATE TABLE IF NOT EXISTS dub_history (
+ id TEXT PRIMARY KEY,
+ filename TEXT,
+ duration REAL,
+ segments_count INTEGER,
+ language TEXT,
+ language_code TEXT,
+ tracks TEXT DEFAULT '[]',
+ job_data TEXT,
+ created_at REAL
+ );
+ """)
+ conn.commit()
+ conn.close()
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# APP LIFECYCLE
+# ═══════════════════════════════════════════════════════════════════════
+
+def get_best_device():
+ if torch.cuda.is_available():
+ return "cuda"
+ if torch.backends.mps.is_available():
+ return "mps"
+ return "cpu"
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ global model
+ _init_db()
+ device = get_best_device()
+ print(f"Loading OmniVoice model on device: {device}...")
+ checkpoint = os.environ.get("OMNIVOICE_MODEL", "k2-fsa/OmniVoice")
+ model = OmniVoice.from_pretrained(
+ checkpoint, device_map=device, dtype=torch.float16, load_asr=True,
+ )
+
+ # Skip MPS warmup — it consumes too much memory on Apple Silicon
+ # and the first real inference will warm things up naturally
+
+ # Only apply torch.compile on CUDA (MPS compile causes GPU thrashing)
+ try:
+ if device == "cuda":
+ model.llm = torch.compile(model.llm, mode="reduce-overhead")
+ print("torch.compile applied.")
+ except Exception as e:
+ print(f"torch.compile skipped: {e}")
+
+ print("OmniVoice model loaded successfully.")
+ yield
+ model = None
+
+
+from fastapi.middleware.cors import CORSMiddleware
+
+app = FastAPI(title="OmniVoice Studio API", version="0.4.0", lifespan=lifespan)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], allow_credentials=True,
+ allow_methods=["*"], allow_headers=["*"],
+ expose_headers=["Content-Disposition"],
+)
+
+# Serve generated audio files statically
+app.mount("/audio", StaticFiles(directory=OUTPUTS_DIR), name="audio")
+app.mount("/voice_audio", StaticFiles(directory=VOICES_DIR), name="voice_audio")
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# VOICE PROFILES (SQLite + disk)
+# ═══════════════════════════════════════════════════════════════════════
+
+@app.get("/profiles")
+async def list_profiles():
+ conn = _get_db()
+ rows = conn.execute("SELECT * FROM voice_profiles ORDER BY created_at DESC").fetchall()
+ conn.close()
+ return [dict(r) for r in rows]
+
+
+@app.post("/profiles")
+async def create_profile(
+ name: str = Form(...),
+ ref_audio: UploadFile = File(...),
+ ref_text: str = Form(""),
+ instruct: str = Form(""),
+ language: str = Form("Auto"),
+):
+ profile_id = str(uuid.uuid4())[:8]
+ ext = os.path.splitext(ref_audio.filename or ".wav")[1]
+ audio_filename = f"{profile_id}{ext}"
+ audio_path = os.path.join(VOICES_DIR, audio_filename)
+
+ with open(audio_path, "wb") as f:
+ f.write(await ref_audio.read())
+
+ conn = _get_db()
+ conn.execute(
+ "INSERT INTO voice_profiles (id, name, ref_audio_path, ref_text, instruct, language, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ (profile_id, name, audio_filename, ref_text, instruct, language, time.time())
+ )
+ conn.commit()
+ conn.close()
+
+ return {"id": profile_id, "name": name}
+
+
+@app.delete("/profiles/{profile_id}")
+async def delete_profile(profile_id: str):
+ conn = _get_db()
+ row = conn.execute("SELECT ref_audio_path FROM voice_profiles WHERE id=?", (profile_id,)).fetchone()
+ if row and row["ref_audio_path"]:
+ path = os.path.join(VOICES_DIR, row["ref_audio_path"])
+ if os.path.exists(path):
+ os.remove(path)
+ conn.execute("DELETE FROM voice_profiles WHERE id=?", (profile_id,))
+ conn.commit()
+ conn.close()
+ return {"deleted": profile_id}
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# GENERATION HISTORY (SQLite + disk)
+# ═══════════════════════════════════════════════════════════════════════
+
+@app.get("/history")
+async def list_history():
+ conn = _get_db()
+ rows = conn.execute("SELECT * FROM generation_history ORDER BY created_at DESC LIMIT 50").fetchall()
+ conn.close()
+ return [dict(r) for r in rows]
+
+
+@app.delete("/history")
+async def clear_history():
+ conn = _get_db()
+ rows = conn.execute("SELECT audio_path FROM generation_history").fetchall()
+ for r in rows:
+ if r["audio_path"]:
+ p = os.path.join(OUTPUTS_DIR, r["audio_path"])
+ if os.path.exists(p):
+ os.remove(p)
+ conn.execute("DELETE FROM generation_history")
+ conn.commit()
+ conn.close()
+ return {"cleared": True}
+
+
+@app.get("/dub/history")
+async def list_dub_history():
+ conn = _get_db()
+ rows = conn.execute("SELECT * FROM dub_history ORDER BY created_at DESC LIMIT 30").fetchall()
+ conn.close()
+ return [dict(r) for r in rows]
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# TTS GENERATION
+# ═══════════════════════════════════════════════════════════════════════
+
+def _run_inference(
+ text, language, ref_audio_path, ref_text, instruct, duration,
+ num_step, guidance_scale, speed, t_shift, denoise,
+ postprocess_output, layer_penalty_factor, position_temperature,
+ class_temperature,
+):
+ audios = model.generate(
+ text=text, language=language, ref_audio=ref_audio_path,
+ ref_text=ref_text, instruct=instruct, duration=duration,
+ num_step=num_step, guidance_scale=guidance_scale, speed=speed,
+ t_shift=t_shift, denoise=denoise, postprocess_output=postprocess_output,
+ layer_penalty_factor=layer_penalty_factor,
+ position_temperature=position_temperature,
+ class_temperature=class_temperature,
+ )
+ return audios[0] # shape (1, T)
+
+
+@app.post("/generate")
+async def generate_speech(
+ text: str = Form(...),
+ language: Optional[str] = Form(None),
+ ref_audio: Optional[UploadFile] = File(None),
+ ref_text: Optional[str] = Form(None),
+ instruct: Optional[str] = Form(None),
+ duration: Optional[float] = Form(None),
+ num_step: int = Form(16),
+ guidance_scale: float = Form(2.0),
+ speed: float = Form(1.0),
+ t_shift: float = Form(0.1),
+ denoise: bool = Form(True),
+ postprocess_output: bool = Form(True),
+ layer_penalty_factor: float = Form(5.0),
+ position_temperature: float = Form(5.0),
+ class_temperature: float = Form(0.0),
+ profile_id: Optional[str] = Form(None),
+):
+ if model is None:
+ raise HTTPException(status_code=503, detail="Model not loaded")
+
+ ref_audio_path = None
+ cleanup_ref = False
+
+ # Load from voice profile if specified
+ if profile_id:
+ conn = _get_db()
+ row = conn.execute("SELECT * FROM voice_profiles WHERE id=?", (profile_id,)).fetchone()
+ conn.close()
+ if row:
+ ref_audio_path = os.path.join(VOICES_DIR, row["ref_audio_path"])
+ if not ref_text:
+ ref_text = row["ref_text"]
+ if not instruct:
+ instruct = row["instruct"]
+ if not language or language == "Auto":
+ language = row["language"] if row["language"] != "Auto" else None
+ elif ref_audio is not None:
+ try:
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f:
+ f.write(await ref_audio.read())
+ ref_audio_path = f.name
+ cleanup_ref = True
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+ start_time = time.time()
+ try:
+ loop = asyncio.get_event_loop()
+ audio_tensor = await loop.run_in_executor(
+ _inference_pool, _run_inference,
+ text, language, ref_audio_path, ref_text, instruct, duration,
+ num_step, guidance_scale, speed, t_shift, denoise,
+ postprocess_output, layer_penalty_factor, position_temperature,
+ class_temperature,
+ )
+ gen_time = round(time.time() - start_time, 2)
+
+ # Save to disk + DB
+ audio_id = str(uuid.uuid4())[:8]
+ audio_filename = f"{audio_id}.wav"
+ audio_path = os.path.join(OUTPUTS_DIR, audio_filename)
+ torchaudio.save(audio_path, audio_tensor, model.sampling_rate)
+
+ audio_dur = round(audio_tensor.shape[-1] / model.sampling_rate, 2)
+
+ conn = _get_db()
+ conn.execute(
+ "INSERT INTO generation_history (id, text, mode, language, instruct, profile_id, audio_path, duration_seconds, generation_time, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
+ (audio_id, text[:200], "clone" if ref_audio_path else "design",
+ language or "Auto", instruct or "", profile_id or "",
+ audio_filename, audio_dur, gen_time, time.time())
+ )
+ conn.commit()
+ conn.close()
+
+ # Also return the WAV bytes for immediate playback
+ buffer = io.BytesIO()
+ torchaudio.save(buffer, audio_tensor, model.sampling_rate, format="wav")
+ buffer.seek(0)
+ return Response(
+ content=buffer.read(), media_type="audio/wav",
+ headers={"X-Audio-Id": audio_id, "X-Gen-Time": str(gen_time), "X-Audio-Path": audio_filename}
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Inference failed: {str(e)}")
+ finally:
+ if cleanup_ref and ref_audio_path and os.path.exists(ref_audio_path):
+ os.remove(ref_audio_path)
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# VIDEO DUBBING PIPELINE
+# ═══════════════════════════════════════════════════════════════════════
+
+def _find_ffmpeg():
+ for path in ["/opt/homebrew/bin/ffmpeg", "/usr/local/bin/ffmpeg", "ffmpeg"]:
+ if shutil.which(path):
+ return path
+ raise RuntimeError("ffmpeg not found")
+
+
+def _find_ffprobe():
+ for path in ["/opt/homebrew/bin/ffprobe", "/usr/local/bin/ffprobe", "ffprobe"]:
+ if shutil.which(path):
+ return path
+ raise RuntimeError("ffprobe not found")
+
+
+@app.post("/dub/upload")
+async def dub_upload(video: UploadFile = File(...)):
+ job_id = str(uuid.uuid4())[:8]
+ job_dir = os.path.join(DUB_DIR, job_id)
+ os.makedirs(job_dir, exist_ok=True)
+
+ ext = os.path.splitext(video.filename or "video.mp4")[1]
+ video_path = os.path.join(job_dir, f"original{ext}")
+ with open(video_path, "wb") as f:
+ f.write(await video.read())
+
+ audio_path = os.path.join(job_dir, "audio.wav")
+ ffmpeg = _find_ffmpeg()
+ try:
+ subprocess.run([
+ ffmpeg, "-i", video_path, "-vn", "-acodec", "pcm_s16le",
+ "-ar", "16000", "-ac", "1", audio_path, "-y"
+ ], check=True, capture_output=True, timeout=120)
+ except subprocess.CalledProcessError as e:
+ raise HTTPException(status_code=500, detail=f"ffmpeg failed: {e.stderr.decode()}")
+
+ ffprobe = _find_ffprobe()
+ try:
+ result = subprocess.run([
+ ffprobe, "-v", "error", "-show_entries", "format=duration",
+ "-of", "json", video_path
+ ], capture_output=True, text=True, timeout=30)
+ dur = float(json.loads(result.stdout)["format"]["duration"])
+ except Exception:
+ dur = 0.0
+
+ # Run demucs to isolate vocals vs background music
+ vocals_path = os.path.join(job_dir, "vocals.wav")
+ no_vocals_path = os.path.join(job_dir, "no_vocals.wav")
+ try:
+ # Run demucs CLI to strictly output 2 stems
+ subprocess.run([
+ "uv", "run", "demucs", "--two-stems", "vocals", "-n", "htdemucs", "-d", "mps",
+ audio_path, "-o", job_dir
+ ], check=True, capture_output=True, timeout=300)
+
+ # Demucs creates an output structure: htdemucs/audio/vocals.wav
+ demucs_out = os.path.join(job_dir, "htdemucs", "audio")
+ if os.path.exists(os.path.join(demucs_out, "vocals.wav")):
+ import shutil
+ shutil.move(os.path.join(demucs_out, "vocals.wav"), vocals_path)
+ shutil.move(os.path.join(demucs_out, "no_vocals.wav"), no_vocals_path)
+ # Remove demucs temp dir
+ shutil.rmtree(os.path.join(job_dir, "htdemucs"))
+ except Exception as e:
+ logger.warning(f"Demucs failed, falling back to mixed audio. {e}")
+ vocals_path = audio_path
+ no_vocals_path = None
+
+ _dub_jobs[job_id] = {
+ "video_path": video_path,
+ "audio_path": audio_path,
+ "vocals_path": vocals_path,
+ "no_vocals_path": no_vocals_path,
+ "duration": dur, "filename": video.filename,
+ "segments": None, "dubbed_tracks": {},
+ }
+ return {"job_id": job_id, "duration": round(dur, 2), "filename": video.filename}
+
+
+def _get_job(job_id: str):
+ if job_id in _dub_jobs:
+ return _dub_jobs[job_id]
+ conn = _get_db()
+ row = conn.execute("SELECT job_data FROM dub_history WHERE id=?", (job_id,)).fetchone()
+ conn.close()
+ if row and row["job_data"]:
+ try:
+ job = json.loads(row["job_data"])
+ _dub_jobs[job_id] = job
+ return job
+ except:
+ pass
+ return None
+
+@app.post("/dub/transcribe/{job_id}")
+async def dub_transcribe(job_id: str):
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+ if model is None or model._asr_pipe is None:
+ raise HTTPException(status_code=503, detail="ASR not loaded")
+
+ def _transcribe():
+ import re
+ # Load pure vocal audio as numpy array for vastly improved Whisper accuracy
+ asr_audio_target = job.get("vocals_path", job.get("audio_path"))
+ audio_np, sr = sf.read(asr_audio_target, dtype="float32")
+ if audio_np.ndim > 1:
+ audio_np = audio_np.mean(axis=1)
+ audio_input = {"array": audio_np, "sampling_rate": sr}
+
+ # Use chunk-level timestamps (lightweight on MPS) then split into sentences
+ result = model._asr_pipe(
+ audio_input, return_timestamps=True,
+ chunk_length_s=15, batch_size=1,
+ )
+
+ # Split chunks into sentences using punctuation
+ sentence_enders = re.compile(r'(?<=[.!?。?!])\s+')
+ segments = []
+
+ if "chunks" in result:
+ for chunk in result["chunks"]:
+ ts = chunk.get("timestamp", (0, 0))
+ chunk_start = ts[0] if ts[0] is not None else 0.0
+ chunk_end = ts[1] if ts[1] is not None else chunk_start + 1.0
+ chunk_text = chunk.get("text", "").strip()
+
+ if not chunk_text:
+ continue
+
+ # Split this chunk into sentences
+ sentences = sentence_enders.split(chunk_text)
+ sentences = [s.strip() for s in sentences if s.strip()]
+
+ if len(sentences) <= 1:
+ segments.append({
+ "start": round(chunk_start, 2),
+ "end": round(chunk_end, 2),
+ "text": chunk_text,
+ })
+ else:
+ # Distribute time proportionally across sentences
+ total_chars = sum(len(s) for s in sentences)
+ chunk_dur = chunk_end - chunk_start
+ t = chunk_start
+ for sent in sentences:
+ ratio = len(sent) / max(total_chars, 1)
+ sent_dur = chunk_dur * ratio
+ segments.append({
+ "start": round(t, 2),
+ "end": round(t + sent_dur, 2),
+ "text": sent,
+ })
+ t += sent_dur
+ else:
+ segments.append({"start": 0.0, "end": job["duration"], "text": result.get("text", "").strip()})
+
+ # Store full transcript
+ job["full_transcript"] = " ".join(s["text"] for s in segments)
+
+ # Free MPS memory
+ if torch.backends.mps.is_available():
+ torch.mps.empty_cache()
+
+ return segments
+
+ loop = asyncio.get_event_loop()
+ segments = await loop.run_in_executor(_inference_pool, _transcribe)
+ job["segments"] = segments
+ return {
+ "job_id": job_id,
+ "segments": segments,
+ "full_transcript": job.get("full_transcript", ""),
+ }
+
+
+class DubSegment(BaseModel):
+ start: float
+ end: float
+ text: str
+ instruct: str = "" # Per-segment voice override
+ profile_id: str = "" # Per-segment voice profile
+
+
+class DubRequest(BaseModel):
+ segments: List[DubSegment]
+ language: str = "Auto"
+ language_code: str = "und" # ISO 639-1 for ffmpeg metadata (e.g. "es", "fr", "de")
+ instruct: str = ""
+ num_step: int = 16
+ guidance_scale: float = 2.0
+ speed: float = 1.0
+
+
+@app.post("/dub/generate/{job_id}")
+async def dub_generate(job_id: str, req: DubRequest):
+ """Generate TTS per segment. Returns SSE progress stream."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+
+ async def _stream():
+ total = len(req.segments)
+ all_segment_wavs = []
+
+ for i, seg in enumerate(req.segments):
+ yield f"data: {json.dumps({'type': 'progress', 'current': i, 'total': total, 'text': seg.text[:50]})}\n\n"
+
+ seg_duration = seg.end - seg.start
+ if seg_duration <= 0.05 or not seg.text.strip():
+ sr = model.sampling_rate
+ silence = torch.zeros(1, int(seg_duration * sr))
+ all_segment_wavs.append((seg.start, seg.end, silence, sr))
+ continue
+
+ def _gen(text, lang, instruct_str, dur_s, nstep, cfg, spd, profile_id=None):
+ ref_audio = None
+ ref_text = None
+ # Load per-segment voice profile if specified
+ if profile_id:
+ conn = _get_db()
+ row = conn.execute("SELECT * FROM voice_profiles WHERE id=?", (profile_id,)).fetchone()
+ conn.close()
+ if row:
+ ref_audio = os.path.join(VOICES_DIR, row["ref_audio_path"])
+ ref_text = row["ref_text"]
+ if not instruct_str:
+ instruct_str = row["instruct"]
+ return model.generate(
+ text=text, language=lang if lang != "Auto" else None,
+ ref_audio=ref_audio, ref_text=ref_text,
+ instruct=instruct_str if instruct_str else None,
+ duration=dur_s, num_step=nstep, guidance_scale=cfg,
+ speed=spd, denoise=True, postprocess_output=True,
+ )[0]
+
+ # Use per-segment instruct/profile if set, otherwise fall back to request-level
+ seg_instruct = seg.instruct or req.instruct
+ seg_profile = seg.profile_id or None
+
+ loop = asyncio.get_event_loop()
+ try:
+ audio_tensor = await loop.run_in_executor(
+ _inference_pool, _gen,
+ seg.text, req.language, seg_instruct, seg_duration,
+ req.num_step, req.guidance_scale, req.speed, seg_profile,
+ )
+ # Save individual segment WAV for preview
+ seg_wav_path = os.path.join(DUB_DIR, job_id, f"seg_{i}.wav")
+ torchaudio.save(seg_wav_path, audio_tensor, model.sampling_rate)
+ all_segment_wavs.append((seg.start, seg.end, audio_tensor, model.sampling_rate))
+ except Exception as e:
+ yield f"data: {json.dumps({'type': 'error', 'segment': i, 'error': str(e)})}\n\n"
+ sr = model.sampling_rate
+ all_segment_wavs.append((seg.start, seg.end, torch.zeros(1, int(seg_duration * sr)), sr))
+
+ yield f"data: {json.dumps({'type': 'assembling'})}\n\n"
+
+ sr = model.sampling_rate
+ total_samples = int(job["duration"] * sr)
+ full_audio = torch.zeros(1, total_samples)
+
+ for start, end, wav, _ in all_segment_wavs:
+ s = int(start * sr)
+ wl = wav.shape[-1]
+ e = min(s + wl, total_samples)
+ full_audio[:, s:e] = wav[:, :e - s]
+
+ # Save this dubbed track with the language code
+ lang_code = req.language_code or "und"
+ track_path = os.path.join(DUB_DIR, job_id, f"dubbed_{lang_code}.wav")
+ torchaudio.save(track_path, full_audio, sr)
+ job["dubbed_tracks"][lang_code] = {
+ "path": track_path,
+ "language": req.language,
+ "language_code": lang_code,
+ }
+
+ # Save to dub_history
+ try:
+ conn = _get_db()
+ conn.execute(
+ "INSERT OR REPLACE INTO dub_history (id, filename, duration, segments_count, language, language_code, tracks, job_data, created_at) VALUES (?,?,?,?,?,?,?,?,?)",
+ (job_id, job.get("filename", ""), job.get("duration", 0), total,
+ req.language, lang_code, json.dumps(list(job["dubbed_tracks"].keys())),
+ json.dumps(job, default=str), time.time())
+ )
+ conn.commit()
+ conn.close()
+ except Exception as e:
+ logger.error(f"Failed to save dub history: {e}")
+
+ yield f"data: {json.dumps({'type': 'done', 'segments_processed': total, 'language_code': lang_code, 'tracks': list(job['dubbed_tracks'].keys())})}\n\n"
+
+ return StreamingResponse(_stream(), media_type="text/event-stream")
+
+
+@app.get("/dub/tracks/{job_id}")
+async def dub_list_tracks(job_id: str):
+ """List all dubbed language tracks for a job."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+ return {"tracks": job.get("dubbed_tracks", {})}
+
+
+@app.get("/dub/download/{job_id}")
+@app.get("/dub/download/{job_id}/{filename}")
+async def dub_download(job_id: str, preserve_bg: bool = Query(True, description="Mix background noise into dubbed tracks"), make_default: bool = Query(True)):
+ """Mux ALL dubbed language tracks into the video.
+ If preserve_bg=true, mixes isolated background noise seamlessly into each dubbed string.
+ If make_default=true, sets the FIRST dubbed language track as the default audio track."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+
+ tracks = job.get("dubbed_tracks", {})
+ if not tracks:
+ raise HTTPException(status_code=400, detail="No dubbed tracks generated yet")
+
+ video_path = job["video_path"]
+ output_path = os.path.join(DUB_DIR, job_id, "dubbed_video_final.mp4")
+ ffmpeg = _find_ffmpeg()
+
+ cmd = [ffmpeg, "-i", video_path]
+ input_idx = 1
+
+ bg_audio = job.get("no_vocals_path") if preserve_bg else None
+ bg_idx = None
+ if bg_audio and os.path.exists(bg_audio):
+ cmd += ["-i", bg_audio]
+ bg_idx = input_idx
+ input_idx += 1
+
+ tracks_to_process = []
+ for lang_code, track_info in tracks.items():
+ cmd += ["-i", track_info["path"]]
+ tracks_to_process.append({"lang_code": lang_code, "idx": input_idx, "info": track_info})
+ input_idx += 1
+
+ # Map original video and original audio
+ cmd += ["-map", "0:v:0", "-map", "0:a:0"]
+
+ if bg_idx is not None:
+ filters = []
+ for i, t in enumerate(tracks_to_process):
+ out_label = f"[aout{i}]"
+ # Normalize mixing so neither drops off unexpectedly
+ filters.append(f"[{bg_idx}:a][{t['idx']}:a]amix=inputs=2:duration=longest:dropout_transition=2:weights=0.8 1.2{out_label}")
+ t["out_label"] = out_label
+ cmd += ["-filter_complex", ";".join(filters)]
+ for t in tracks_to_process:
+ cmd += ["-map", t["out_label"]]
+ else:
+ for t in tracks_to_process:
+ cmd += ["-map", f"{t['idx']}:a:0"]
+
+ if bg_idx is not None:
+ cmd += ["-c:v", "copy", "-c:a", "aac", "-b:a", "192k"]
+ else:
+ cmd += ["-c:v", "copy", "-c:a", "aac", "-b:a", "192k"]
+
+ cmd += ["-metadata:s:a:0", "language=und", "-metadata:s:a:0", "title=Original"]
+
+ for idx, t in enumerate(tracks_to_process):
+ stream_idx = idx + 1
+ cmd += [
+ f"-metadata:s:a:{stream_idx}", f"language={t['lang_code']}",
+ f"-metadata:s:a:{stream_idx}", f"title={t['info']['language']}"
+ ]
+
+ # Explicit default audio tracks handling
+ if make_default and tracks_to_process:
+ cmd += ["-disposition:a:0", "0"] # Remove default from original
+ cmd += ["-disposition:a:1", "default"] # Give default to first dub track
+ else:
+ cmd += ["-disposition:a:0", "default"]
+
+ cmd += ["-shortest", output_path, "-y"]
+
+ try:
+ subprocess.run(cmd, check=True, capture_output=True, timeout=300)
+ except subprocess.CalledProcessError as e:
+ raise HTTPException(status_code=500, detail=f"ffmpeg mux failed: {e.stderr.decode()}")
+
+ base_name = os.path.splitext(job.get('filename', 'output'))[0]
+ safe_name = ''.join(c for c in base_name if c.isalnum() or c in '-_ ').strip() or 'output'
+ dl_name = f"dubbed_{safe_name}.mp4"
+ return FileResponse(
+ output_path, media_type="video/mp4",
+ headers={"Content-Disposition": f'attachment; filename="{dl_name}"'},
+ )
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# TRANSLATION
+# ═══════════════════════════════════════════════════════════════════════
+
+# Google Translate language codes for common dub targets
+TRANSLATE_CODES = {
+ "en": "en", "es": "es", "fr": "fr", "de": "de", "it": "it", "pt": "pt",
+ "ru": "ru", "ja": "ja", "ko": "ko", "zh": "zh-CN", "ar": "ar", "hi": "hi",
+ "tr": "tr", "pl": "pl", "nl": "nl", "sv": "sv", "th": "th", "vi": "vi",
+ "id": "id", "uk": "uk",
+}
+
+
+class TranslateRequest(BaseModel):
+ segments: List[dict] # [{"id": 0, "text": "..."}]
+ target_lang: str # ISO 639-1 code like "es", "fr"
+
+
+@app.post("/dub/translate")
+async def dub_translate(req: TranslateRequest):
+ """Translate all segment texts to the target language using Google Translate."""
+ from deep_translator import GoogleTranslator
+
+ lang_code = TRANSLATE_CODES.get(req.target_lang, req.target_lang)
+
+ def _translate():
+ translator = GoogleTranslator(source="auto", target=lang_code)
+ results = []
+ for seg in req.segments:
+ try:
+ translated = translator.translate(seg["text"])
+ results.append({"id": seg["id"], "text": translated or seg["text"]})
+ except Exception as e:
+ results.append({"id": seg["id"], "text": seg["text"], "error": str(e)})
+ return results
+
+ loop = asyncio.get_event_loop()
+ translated = await loop.run_in_executor(None, _translate)
+ return {"translated": translated, "target_lang": req.target_lang}
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# SEGMENT PREVIEW
+# ═══════════════════════════════════════════════════════════════════════
+
+@app.get("/dub/preview/{job_id}/{segment_index}")
+async def dub_preview_segment(job_id: str, segment_index: int):
+ """Return the WAV for a single dubbed segment (generated during /dub/generate)."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+ seg_path = os.path.join(DUB_DIR, job_id, f"seg_{segment_index}.wav")
+ if not os.path.exists(seg_path):
+ raise HTTPException(status_code=404, detail="Segment not generated yet")
+ return FileResponse(seg_path, media_type="audio/wav")
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# AUDIO-ONLY DOWNLOAD (timestamp-synced)
+# ═══════════════════════════════════════════════════════════════════════
+
+@app.get("/dub/download-audio/{job_id}")
+@app.get("/dub/download-audio/{job_id}/{filename}")
+async def dub_download_audio(job_id: str, lang: str = Query(None), preserve_bg: bool = Query(True)):
+ """Download just the dubbed audio track (WAV). Timestamp-synced with original video."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+
+
+ tracks = job.get("dubbed_tracks", {})
+
+ if lang and lang in tracks:
+ wav_path = tracks[lang]["path"]
+ elif tracks:
+ # Return first available track
+ wav_path = list(tracks.values())[0]["path"]
+ else:
+ raise HTTPException(status_code=400, detail="No dubbed audio track generated yet")
+
+ if not os.path.exists(wav_path):
+ raise HTTPException(status_code=404, detail="Audio file not found")
+
+ lang_label = lang or list(tracks.keys())[0]
+ base_name = os.path.splitext(job.get('filename', 'audio'))[0]
+
+ bg_audio = job.get("no_vocals_path") if preserve_bg else None
+ if bg_audio and os.path.exists(bg_audio):
+ ffmpeg = _find_ffmpeg()
+ final_audio_path = os.path.join(DUB_DIR, job_id, f"mixed_dub_{lang_label}.wav")
+ cmd = [
+ ffmpeg, "-i", bg_audio, "-i", wav_path,
+ "-filter_complex", "[0:a][1:a]amix=inputs=2:duration=longest:dropout_transition=2:weights=0.8 1.2[aout]",
+ "-map", "[aout]", "-c:a", "pcm_s16le", "-y", final_audio_path
+ ]
+ try:
+ subprocess.run(cmd, check=True, capture_output=True, timeout=120)
+ wav_path = final_audio_path
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Failed to mix audio: {e.stderr.decode()}")
+
+ base_name = os.path.splitext(job.get('filename', 'audio'))[0]
+ safe_name = ''.join(c for c in base_name if c.isalnum() or c in '-_ ').strip() or 'audio'
+ dl_name = f"dubbed_audio_{lang_label}_{safe_name}.wav"
+ return FileResponse(
+ wav_path, media_type="audio/wav",
+ headers={"Content-Disposition": f'attachment; filename="{dl_name}"'},
+ )
+
+
+# ═══════════════════════════════════════════════════════════════════════
+# SRT SUBTITLE EXPORT
+# ═══════════════════════════════════════════════════════════════════════
+
+def _format_srt_time(seconds):
+ """Format seconds as SRT timestamp: HH:MM:SS,mmm"""
+ h = int(seconds // 3600)
+ m = int((seconds % 3600) // 60)
+ s = int(seconds % 60)
+ ms = int((seconds % 1) * 1000)
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
+
+
+@app.get("/dub/srt/{job_id}")
+@app.get("/dub/srt/{job_id}/{filename}")
+async def dub_export_srt(job_id: str):
+ """Export transcript segments as an SRT subtitle file."""
+ job = _get_job(job_id)
+ if not job:
+ raise HTTPException(status_code=404, detail="Job not found")
+
+
+ segments = job.get("segments", [])
+ if not segments:
+ raise HTTPException(status_code=400, detail="No transcript segments available")
+
+ srt_lines = []
+ for i, seg in enumerate(segments):
+ start_ts = _format_srt_time(seg["start"])
+ end_ts = _format_srt_time(seg["end"])
+ srt_lines.append(f"{i + 1}")
+ srt_lines.append(f"{start_ts} --> {end_ts}")
+ srt_lines.append(seg["text"])
+ srt_lines.append("")
+
+ srt_content = "\n".join(srt_lines)
+
+ base_name = os.path.splitext(job.get('filename', 'video'))[0]
+ return Response(
+ content=srt_content,
+ media_type="text/plain",
+ headers={
+ "Content-Disposition": f'attachment; filename="subtitles_{base_name}.srt"',
+ },
+ )
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/docs/data_preparation.md b/docs/data_preparation.md
new file mode 100644
index 00000000..3ee972ba
--- /dev/null
+++ b/docs/data_preparation.md
@@ -0,0 +1,182 @@
+# Data Preparation
+
+OmniVoice trains on a custom WebDataset format where audio data is packed into **tar shards** with paired **JSONL metadata** files. Each tar shard contains hundreds to thousands of samples (as `.npy` audio token arrays), drastically reducing disk I/O during training. The separated jsonl file allows for easier modification of metadata. This document explains the data format in detail and walks through the preparation pipeline.
+
+
+## 1. Input Format
+
+Prepare a JSONL file where each line is a JSON object:
+
+```jsonl
+{"id": "sample_001", "audio_path": "/data/audio/001.wav", "text": "Hello world", "language_id": "en"}
+{"id": "sample_002", "audio_path": "/data/audio/002.wav", "text": "你好世界", "language_id": "zh"}
+```
+
+Fields:
+- `id` — unique sample identifier (used to match samples across shards and label files)
+- `audio_path` — absolute path to the audio file (wav/flac/mp3, will be resampled to 24 kHz)
+- `text` — transcript text
+- `language_id` — (optional) language code, used for multilingual training, can be omitted
+
+
+## 2. Processing
+
+The tokenization script `extract_audio_tokens.py` converts audio into 8-layer discrete tokens and packs them into WebDataset shards.
+
+```bash
+export CUDA_VISIBLE_DEVICES="0,1,2,4" # GPUs used for token extraction
+python -m omnivoice.scripts.extract_audio_tokens \
+ --input_jsonl data.jsonl \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl \
+ --tokenizer_path eustlb/higgs-audio-v2-tokenizer \
+ --nj_per_gpu 3 \
+ --shuffle True
+```
+
+What it does:
+1. Reads your JSONL manifest
+2. Encodes each audio file into discrete tokens using audio tokenizer
+3. Packs tokens into WebDataset tar shards with paired jsonl metadata files
+4. Generates a `data.lst` manifest file
+
+
+Alternative: WebDataset Input (if you already have raw-audio tar shards)
+
+Pass the `data.lst` manifest instead of `--input_jsonl`:
+
+```bash
+export CUDA_VISIBLE_DEVICES="0,1,2,4" # GPUs used for token extraction
+python -m omnivoice.scripts.extract_audio_tokens \
+ --input_manifest existing_data/data.lst \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl \
+ --tokenizer_path eustlb/higgs-audio-v2-tokenizer \
+ --nj_per_gpu 3 \
+ --shuffle True
+```
+
+The existing_data/data.lst is generated with:
+```bash
+python -m omnivoice.scripts.jsonl_to_webdataset \
+ --input data.jsonl \
+ --output data/shards \
+ --sr 24000 \
+ --shard-size 1000
+```
+
+This resamples audio to the target sample rate and packs FLAC files into tar shards with paired jsonl metadata files.
+
+
+
+
+
+### Explanation of the script's options:
+
+| Option | Default | Description |
+|---|---|---|
+| `--input_manifest` | None | Path to input dataset manifest (`data.lst`), mutually exclusive with `--input_jsonl` |
+| `--input_jsonl` | None | Path to raw JSONL file, mutually exclusive with `--input_manifest` |
+| `--tar_output_pattern` | (required) | Tar shard output pattern, e.g. `output/audios/shard-%06d.tar` |
+| `--jsonl_output_pattern` | (required) | JSONL shard output pattern, e.g. `output/txts/shard-%06d.jsonl` |
+| `--tokenizer_path` | `eustlb/higgs-audio-v2-tokenizer` | HuggingFace tokenizer path or local path |
+| `--nj_per_gpu` | 3 | Worker processes per GPU |
+| `--loader_workers` | 24 | DataLoader workers for streaming `IterableDataset` |
+| `--shuffle` | True | Shuffle samples before sharding |
+| `--shuffle-seed` | 42 | Random seed for shuffling |
+| `--samples_per_shard` | 1000 | Max samples per tar shard |
+| `--min_num_shards` | 32 | Minimum number of output shards (ensures shard count >= num\_gpu × num\_workers) |
+| `--min_length` | 0.0 | Skip audio shorter than this (seconds) |
+| `--max_length` | inf | Skip audio longer than this (seconds) |
+| `--skip_errors` | False | Continue on processing errors instead of aborting |
+| `--num_machines` | 1 | Total number of machines for distributed runs |
+| `--machine_index` | 0 | Zero-based machine index for distributed preprocessing |
+
+
+### Output Structure
+
+Output structure with the following output patterns
+
+```bash
+--tar_output_pattern output/audios/shard-%06d.tar \
+--jsonl_output_pattern output/txts/shard-%06d.jsonl
+```
+
+will be:
+
+```
+output/
+├── audios/ # WebDataset tar shards (audio tokens)
+│ ├── shard-000000.tar # Each tar packs ~1000 samples
+│ ├── shard-000001.tar
+│ └── ...
+├── txts/ # Per-shard companion JSONL labels
+│ ├── shard-000000.jsonl # One JSON line per sample in the corresponding tar
+│ ├── shard-000001.jsonl
+│ └── ...
+├── data.lst # Manifest linking tar ↔ jsonl shards
+└── errors.jsonl # Samples that failed processing (if any)
+```
+
+`data.lst` and `errors.jsonl` are written to the **parent directory** of `audios/` and `txts/`.
+
+
+### The `data.lst` manifest
+
+Each line in `data.lst` describes one shard:
+
+```
+/path/to/shard-000000.tar /path/to/shard-000000.jsonl 1000 3600.500
+/path/to/shard-000001.tar /path/to/shard-000001.jsonl 800 2880.200
+```
+
+Format: ` `
+
+- Paths are **absolute**
+- `.tar` file contains the audio tokens.
+- `.jsonl` file contains the metadata in the original provided JSONL file, allows easier access and modification of metadata without decompressing the tar file.
+- This manifest is what the training data config references.
+
+### Inside a tar shard
+
+Each `.tar` file packs **many samples** (default 1000 per shard) into a single archive. This is the key advantage of WebDataset: instead of reading thousands of tiny files, the dataloader reads sequentially from a few large tars, drastically reducing disk I/O pressure.
+
+Each sample in the tar is a pair of files with matching keys:
+
+```
+shard-000000.tar:
+ sample_001.npy # Audio tokens: numpy array, shape [8, T], dtype int16
+ sample_002.npy
+ ...
+ sample_1000.npy
+```
+
+## 3. Data Config for Training
+
+After creating WebDataset shards, write a data config JSON that references them:
+
+```json
+{
+ "train": [
+ {
+ "language_id": "en",
+ "manifest_path": ["data/custom/tokens/train/data.lst"],
+ "repeat": 1
+ }
+ ],
+ "dev": [
+ {
+ "language_id": "en",
+ "manifest_path": ["data/custom/tokens/dev/data.lst"],
+ "repeat": 1
+ }
+ ]
+}
+```
+- `manifest_path` — list of `data.lst` files (one per shard directory)
+- `repeat` — how many times to repeat this dataset per epoch (useful for balancing languages)
+- `language_id` is not used, just for a better data organization.
+
+See [examples/config/](../examples/config/) for ready-to-use data config files.
+
+> See [docs/data_preparation_advanced.md](../docs/data_preparation_advanced.md) for denoising and noise augmentation.
\ No newline at end of file
diff --git a/docs/data_preparation_advanced.md b/docs/data_preparation_advanced.md
new file mode 100644
index 00000000..e7cc3cad
--- /dev/null
+++ b/docs/data_preparation_advanced.md
@@ -0,0 +1,67 @@
+# Advanced Data Preparation
+
+The advanced pipeline adds **denoising** and **prompt noise augmentation** on top of the basic tokenization workflow. Each stage is optional.
+
+## Prerequisites
+
+- **Denoising**: Sidon model checkpoints (`feature_extractor_cuda.pt`, `decoder_cuda.pt`) from https://huggingface.co/sarulab-speech/sidon-v0.1/tree/main.
+- **Noise augmentation**: noise + RIR tar shards with `data.lst` manifests
+
+## Pipeline Overview
+
+```
+Step 1 (optional): Denoise
+ Raw audio → Sidon denoiser → clean audio
+
+Step 2: Tokenize (with optional noise augmentation)
+ Clean audio + noise augment on prefix → audio tokenizer → tokens
+```
+
+
+## Denoise
+
+Use the [Sidon](https://github.com/sarulab-speech/Sidon) speech enhancement model to remove background noise from raw audio.
+
+```bash
+export CUDA_VISIBLE_DEVICES="0,1,2,3"
+python -m omnivoice.scripts.denoise_audio \
+ --input_jsonl data.jsonl \
+ --tar_output_pattern data/denoised/audios/shard-%06d.tar \
+ --jsonl_output_pattern data/denoised/txts/shard-%06d.jsonl \
+ --feature_extractor_path /path/to/sidon_feature_extractor_cuda.pt \
+ --decoder_path /path/to/sidon_decoder_cuda.pt \
+ --target_sample_rate 24000 \
+ --batch_duration 200.0
+```
+
+What it does:
+1. Reads your JSONL manifest
+2. Runs Sidon denoiser on each audio file
+3. Outputs denoised audio as custom WebDataset tar/jsonl shards
+4. Generates a `data.lst` manifest in `data/denoised/`
+
+> You can also pass `--input_manifest /path/to/data.lst` if you already have a custom webdataset format dataset.
+> The next step would be passing the generated `data.lst` file with `--input_manifest` to `omnivoice.scripts.extract_audio_tokens` for tokens extraction.
+
+
+### Tokenize with noise augmentation
+
+Adds environmental noise and room reverb to **prompt audio** during tokenization, making the model robust to noisy reference audio at inference time. Note that in our model, we only add noise augmentation for a small proportion of data, making sure the model can also generate good audio with clean reference audio.
+
+You need two additional datasets in WebDataset format:
+- **Noise recordings**: environmental noise tar shards with a `data.lst` manifest
+- **Room impulse responses (RIR)**: RIR tar shards with a `data.lst` manifest
+
+```bash
+export CUDA_VISIBLE_DEVICES="0,1,2,4"
+python -m omnivoice.scripts.extract_audio_tokens_add_noise \
+ --input_jsonl data.jsonl \
+ --tar_output_pattern data/tokens/shard-%06d.tar \
+ --jsonl_output_pattern data/txts/shard-%06d.jsonl \
+ --tokenizer_path eustlb/higgs-audio-v2-tokenizer \
+ --noise_manifest data/noise_shards/data.lst \
+ --rir_manifest data/rir_shards/data.lst \
+ --nj_per_gpu 3
+```
+
+> You can also pass `--input_manifest /path/to/data.lst` if you already have a custom webdataset format dataset.
diff --git a/docs/evaluation.md b/docs/evaluation.md
new file mode 100644
index 00000000..71902b2a
--- /dev/null
+++ b/docs/evaluation.md
@@ -0,0 +1,48 @@
+# Evaluation
+
+Evaluate OmniVoice models with standard TTS metrics: WER (intelligibility), SIM-o (speaker similarity), and UTMOS (naturalness).
+
+## Supported Test Sets
+
+| Test Set | Languages | WER Module | Metrics |
+|---|---|---|---|
+| **LibriSpeech-PC** | English | HuBERT WER | WER + Speaker Sim + MOS |
+| **Seed-TTS (en)** | English | Whisper WER | WER + MOS |
+| **Seed-TTS (zh)** | Chinese | Paraformer WER | WER + MOS |
+| **FLEURS** | 102 languages | Omnilingual-ASR WER | WER (per-language + macro-avg) |
+| **MiniMax Multilingual** | 24 languages | Whisper + Paraformer | WER + MOS |
+
+## Prerequisites
+
+```bash
+pip install omnivoice[eval]
+# or
+uv sync --extra eval
+```
+
+
+## Quick Start
+
+```bash
+cd examples
+bash run_eval.sh
+# run_eval.sh will
+# (1) download all required test sets and test models;
+# (2) inference and evaluation for each test set.
+```
+
+## Metrics Explained
+
+### WER (Word Error Rate)
+Measures how intelligible the generated speech is by transcribing it with an ASR model and comparing to the reference text. Lower is better. Note that some languages actually use CER (Character Error Rate).
+
+- **LibriSpeech-PC**: HuBERT-based ASR
+- **Seed-TTS**: Whisper (en) or Paraformer (zh)
+- **MiniMax**: Whisper for non-Chinese, Paraformer for Chinese
+- **FLEURS**: Omnilingual-ASR multilingual model
+
+### Speaker Similarity
+Cosine similarity between speaker embeddings (ECAPA-TDNN + WavLM) of the reference and generated audio. Higher is better.
+
+### UTMOS (Predicted MOS)
+Neural network that predicts Mean Opinion Score from audio. Higher is better.
\ No newline at end of file
diff --git a/docs/generation-parameters.md b/docs/generation-parameters.md
new file mode 100644
index 00000000..6ddef0db
--- /dev/null
+++ b/docs/generation-parameters.md
@@ -0,0 +1,66 @@
+# Generation Parameters
+
+Parameters can be passed as keyword arguments to `model.generate(...)` or via the `OmniVoiceGenerationConfig` dataclass. See below for the full list and which category each belongs to.
+
+```python
+# 1) Direct keyword arguments
+audio = model.generate(text="Hello world", num_step=32, guidance_scale=2.0)
+
+# 2) Via OmniVoiceGenerationConfig dataclass
+from omnivoice import OmniVoiceGenerationConfig
+
+config = OmniVoiceGenerationConfig(num_step=32, guidance_scale=2.0)
+audio = model.generate(text="Hello world", generation_config=config)
+```
+
+## Decoding
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `num_step` | int | 32 | Number of iterative unmasking steps. Higher values improve quality but slow down generation. Use 16 for faster inference. |
+| `denoise` | bool | True | Prepend the `<|denoise|>` token to the input, which signals the model to produce cleaner speech. |
+| `guidance_scale` | float | 2.0 | Classifier-free guidance scale.|
+| `t_shift` | float | 0.1 | Time-step shift for the noise schedule. Smaller values emphasise earlier steps in decoding. |
+
+## Sampling
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `position_temperature` | float | 5.0 | Temperature for mask-position selection. 0 = greedy (deterministic). Higher values increase randomness. |
+| `class_temperature` | float | 0.0 | Temperature for token sampling at each step. 0 = greedy (deterministic). Higher values increase randomness. |
+| `layer_penalty_factor` | float | 5.0 | Penalty applied to deeper codebook layers, encouraging earlier (lower) layers to unmask first. |
+
+## Duration & Speed
+
+These accept a single value applied to all items, or a per-item list (useful in batch mode):
+
+```python
+# Fixed 10-second output
+audio = model.generate(text="Hello, this is a test of duration control", duration=10.0)
+
+# Faster speech (1.2x faster than estimated)
+audio = model.generate(text="Hello, this is a test of duration control", speed=1.2)
+```
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `duration` | float or list[float \| None] | None | Fixed output duration in seconds. Overrides `speed` when set. |
+| `speed` | float or list[float \| None] | None | Speed factor. Values > 1.0 produce shorter audio (faster); values < 1.0 produce longer audio (slower). Ignored when `duration` is set. Defaults to 1.0 when both are None. |
+
+Priority: `duration` > `speed`.
+
+## Pre/Post Processing
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `preprocess_prompt` | bool | True | Whether to apply preprocessing to the voice-clone prompt audio (remove long silences in reference audio, add punctuation in the end of reference text). |
+| `postprocess_output` | bool | True | Apply post-processing to generated audio (remove long silences). |
+
+## Long-Form Generation
+
+To support stable long-form speech generation with low VRAM consumption, the text is automatically split into smaller segments when the estimated duration of the generated speech exceeds `audio_chunk_duration`, with each segment producing approximately `audio_chunk_duration` seconds of audio. This approach allows the model to accept arbitrarily long text and generate arbitrarily long speech with near-constant VRAM consumption.
+
+| Parameter | Type | Default | Description |
+|---|---|---|---|
+| `audio_chunk_duration` | float | 15.0 | Target chunk duration (seconds) when splitting long text. |
+| `audio_chunk_threshold` | float | 30.0 | Estimated audio duration (seconds) above which chunking is activated. |
diff --git a/docs/lang_id_name_map.tsv b/docs/lang_id_name_map.tsv
new file mode 100644
index 00000000..cd496ee0
--- /dev/null
+++ b/docs/lang_id_name_map.tsv
@@ -0,0 +1,647 @@
+language_id language_name iso_639_3_id train_data_duration
+aae Arbëreshë Albanian aae 6.11
+aal Afade aal 10.19
+aao Algerian Saharan Arabic aao 2.02
+ab Abkhazian abk 57.27
+abb Bankon abb 11.2
+abn Abua abn 10.27
+abr Abron abr 9.22
+abs Ambonese Malay abs 10.03
+abv Baharna Arabic abv 10.41
+acm Mesopotamian Arabic acm 3.78
+acw Hijazi Arabic acw 22.32
+acx Omani Arabic acx 22.03
+adf Dhofari Arabic adf 0.31
+adx Amdo Tibetan adx 56.94
+ady Adyghe ady 32.6
+aeb Tunisian Arabic aeb 21.63
+aec Saidi Arabic aec 9.28
+af Afrikaans afr 4.4
+afb Gulf Arabic afb 98.55
+afo Eloyi afo 11.21
+ahl Igo ahl 9.22
+ahs Ashe ahs 10.62
+ajg Aja (Benin) ajg 5.63
+aju Judeo-Moroccan Arabic aju 7.21
+ala Alago ala 11.04
+aln Gheg Albanian aln 3.92
+alo Larike-Wakasihu alo 9.97
+am Amharic amh 12.83
+amu Guerrero Amuzgo amu 10.1
+an Aragonese arg 16.4
+anc Ngas anc 10.14
+ank Goemai ank 10.0
+anp Angika anp 10.65
+anw Anaang anw 9.65
+aom Ömie aom 8.19
+apc Levantine Arabic apc 15.65
+apd Sudanese Arabic apd 9.93
+arb Standard Arabic arb 1483.53
+arq Algerian Arabic arq 9.64
+ars Najdi Arabic ars 203.54
+ary Moroccan Arabic ary 104.67
+arz Egyptian Arabic arz 23.23
+as Assamese asm 270.85
+ast Asturian ast 8.48
+avl Eastern Egyptian Bedawi Arabic avl 1.86
+awo Awak awo 10.22
+ayl Libyan Arabic ayl 20.13
+ayp North Mesopotamian Arabic ayp 10.92
+az Azerbaijani aze 9.84
+ba Bashkir bak 249.1
+bag Tuki bag 10.97
+bas Basa (Cameroon) bas 10.66
+bax Bamun bax 10.24
+bba Baatonum bba 10.53
+bbj Ghomálá' bbj 7.32
+bbl Bats bbl 11.22
+bbu Kulung (Nigeria) bbu 10.39
+bce Bamenyam bce 9.9
+bci Baoulé bci 10.21
+bcs Kohumono bcs 10.45
+bcy Bacama bcy 9.94
+bda Bayot bda 9.47
+bde Bade bde 9.89
+bdm Buduma bdm 10.17
+be Belarusian bel 1809.43
+beb Bebele beb 7.52
+bew Betawi bew 11.15
+bfd Bafut bfd 9.03
+bft Balti bft 16.28
+bg Bulgarian bul 2190.76
+bgp Eastern Balochi bgp 10.98
+bhb Bhili bhb 9.98
+bhh Bukharic bhh 11.38
+bho Bhojpuri bho 10.05
+bhp Bima bhp 10.67
+bhr Bara Malagasy bhr 12.14
+bjj Kanauji bjj 11.01
+bjk Barok bjk 10.16
+bjn Banjar bjn 11.68
+bjt Balanta-Ganja bjt 9.41
+bkh Bakoko bkh 6.0
+bkm Kom (Cameroon) bkm 10.76
+bky Bokyi bky 9.85
+bmm Northern Betsimisaraka Malagasy bmm 19.12
+bmq Bomu bmq 10.68
+bn Bengali ben 271.76
+bnm Batanga bnm 15.01
+bnn Bunun bnn 9.26
+bns Bundeli bns 10.88
+bo Tibetan bod 82.27
+bou Bondei bou 9.98
+bqg Bago-Kusuntu bqg 8.86
+br Breton bre 25.48
+bra Braj bra 10.68
+brh Brahui brh 19.89
+bri Mokpwe bri 7.53
+brx Bodo brx 231.57
+bs Bosnian bos 690.73
+bsh Kati bsh 8.77
+bsj Bangwinji bsj 10.0
+bsk Burushaski bsk 9.14
+btm Batak Mandailing btm 11.09
+btv Bateri btv 9.8
+bug Buginese bug 11.09
+bum Bulu (Cameroon) bum 9.06
+buo Terei buo 9.48
+bux Boghom bux 10.48
+bwr Bura-Pabir bwr 10.4
+bxf Bilur bxf 10.84
+byc Ubaghara byc 11.11
+bys Burak bys 9.92
+byv Medumba byv 10.95
+byx Qaqet byx 9.79
+bzc Southern Betsimisaraka Malagasy bzc 17.45
+bzw Basa (Nigeria) bzw 10.27
+ca Catalan cat 3358.6
+ccg Samba Daka ccg 10.11
+ceb Cebuano ceb 12.17
+cen Cen cen 9.85
+cfa Dijim-Bwilim cfa 10.32
+cgg Chiga cgg 10.84
+chq Quiotepec Chinantec chq 9.76
+cjk Chokwe cjk 11.01
+ckb Central Kurdish ckb 137.52
+ckl Cibak ckl 10.91
+ckr Kairak ckr 10.51
+cky Cakfem-Mushere cky 8.96
+cnh Hakha Chin cnh 2.24
+cpy South Ucayali Ashéninka cpy 9.15
+cs Czech ces 148.13
+cte Tepinapa Chinantec cte 9.54
+ctl Tlacoatzintepec Chinantec ctl 10.04
+cut Teutila Cuicatec cut 8.04
+cux Tepeuxila Cuicatec cux 7.83
+cv Chuvash chv 23.96
+cy Welsh cym 131.21
+da Danish dan 1665.98
+dag Dagbani dag 10.14
+dar Dargwa dar 1.22
+dav Taita dav 9.12
+dbd Dadiya dbd 9.61
+dcc Deccan dcc 10.38
+de German deu 21927.13
+deg Degema deg 11.07
+dgh Dghwede dgh 9.95
+dgo Dogri dgo 117.04
+dje Zarma dje 10.72
+dmk Domaaki dmk 6.38
+dml Dameli dml 9.18
+dru Rukai dru 9.26
+dty Dotyali dty 10.85
+dua Duala dua 12.13
+dv Dhivehi div 38.61
+dyu Dyula dyu 0.34
+dzg Dazaga dzg 9.96
+ebr Ebrié ebr 1.5
+ebu Embu ebu 9.81
+ego Eggon ego 9.95
+eiv Askopan eiv 10.44
+eko Koti eko 8.15
+ekr Yace ekr 10.76
+el Greek ell 2412.54
+elm Eleme elm 11.27
+en English eng 206061.1
+eo Esperanto epo 1396.64
+es Spanish spa 27559.74
+esu Central Yupik esu 2.18
+et Estonian est 960.37
+eto Eton (Cameroon) eto 7.43
+ets Yekhee ets 10.11
+etu Ejagham etu 10.3
+eu Basque eus 479.86
+ewo Ewondo ewo 12.71
+ext Extremaduran ext 13.59
+eyo Keiyo eyo 9.24
+fa Persian fas 366.07
+fan Fang (Equatorial Guinea) fan 3.51
+fat Fanti fat 11.38
+ff Fulah ful 13.84
+ffm Maasina Fulfulde ffm 10.46
+fi Finnish fin 468.62
+fia Nobiin fia 9.96
+fil Filipino fil 7.71
+fip Fipa fip 10.55
+fkk Kirya-Konzəl fkk 9.98
+fmp Fe'fe' fmp 9.86
+fr French fra 23675.32
+fub Adamawa Fulfulde fub 13.12
+fuc Pulaar fuc 14.77
+fue Borgu Fulfulde fue 20.1
+fuf Pular fuf 13.77
+fuh Western Niger Fulfulde fuh 9.69
+fui Bagirmi Fulfulde fui 15.04
+fuq Central-Eastern Niger Fulfulde fuq 9.28
+fuv Nigerian Fulfulde fuv 9.97
+fy Western Frisian fry 70.41
+ga Irish gle 21.4
+gbm Garhwali gbm 19.14
+gbr Gbagyi gbr 12.12
+gby Gbari gby 12.59
+gcc Mali gcc 9.87
+gdf Guduf-Gava gdf 12.21
+gej Gen gej 5.39
+ges Geser-Gorom ges 10.08
+ggg Gurgula ggg 7.12
+gid Gidar gid 10.06
+gig Goaria gig 9.41
+giz South Giziga giz 10.03
+gjk Kachi Koli gjk 20.83
+gju Gujari gju 8.66
+gl Galician glg 208.81
+glw Glavda glw 10.51
+gn Guarani grn 4.06
+gol Gola gol 9.26
+gom Goan Konkani gom 9.82
+gsl Gusilay gsl 10.0
+gu Gujarati guj 91.18
+gui Eastern Bolivian Guaraní gui 22.72
+gur Farefare gur 9.24
+guz Gusii guz 9.5
+gv Manx glv 10.07
+gwc Gawri gwc 10.83
+gwe Gweno gwe 8.87
+gwt Gawar-Bati gwt 12.16
+gya Northwest Gbaya gya 8.45
+gyz Geji gyz 10.49
+ha Hausa hau 17.75
+hah Hahon hah 9.64
+hao Hakö hao 8.56
+haw Hawaiian haw 11.79
+haz Hazaragi haz 9.69
+hbb Huba hbb 10.7
+he Hebrew heb 13.4
+hem Hemba hem 9.53
+hi Hindi hin 117.17
+hia Lamang hia 11.07
+hkk Hunjara-Kaina Ke hkk 8.69
+hla Halia hla 9.86
+hno Northern Hindko hno 20.04
+hoj Hadothi hoj 10.08
+hr Croatian hrv 2795.31
+hsb Upper Sorbian hsb 2.71
+ht Haitian hat 0.04
+hu Hungarian hun 255.83
+hue San Francisco Del Mar Huave hue 9.45
+hul Hula hul 10.33
+hux Nüpode Huitoto hux 9.04
+hwo Hwana hwo 11.23
+hy Armenian hye 42.15
+hz Herero her 9.59
+ia Interlingua (International Auxiliary Language Association) ina 13.48
+ibb Ibibio ibb 7.38
+id Indonesian ind 6327.87
+ida Idakho-Isukha-Tiriki ida 9.31
+idu Idoma idu 11.16
+ig Igbo ibo 13.69
+ijc Izon ijc 9.95
+ijn Kalabari ijn 11.04
+ik Inupiaq ipk 2.11
+ikw Ikwere ikw 10.0
+is Icelandic isl 647.29
+ish Esan ish 10.05
+iso Isoko iso 10.33
+it Italian ita 9402.46
+its Isekiri its 11.85
+itw Ito itw 9.19
+itz Itzá itz 7.08
+ja Japanese jpn 36914.4
+jal Yalahatan jal 11.18
+jax Jambi Malay jax 10.29
+jgo Ngomba jgo 10.15
+jmx Western Juxtlahuaca Mixtec jmx 10.01
+jns Jaunsari jns 11.25
+jqr Jaqaru jqr 9.32
+juk Wapan juk 10.22
+juo Jiba juo 10.43
+jv Javanese jav 11.19
+ka Georgian kat 156.96
+kab Kabyle kab 529.52
+kai Karekare kai 10.52
+kaj Jju kaj 10.16
+kam Kamba kam 14.72
+kbd Kabardian kbd 108.35
+kbl Kanembu kbl 10.19
+kbt Abadi kbt 9.73
+kcq Kamo kcq 10.49
+kdh Tem kdh 4.07
+kea Kabuverdianu kea 10.51
+keu Akebu keu 9.1
+kfe Kota (India) kfe 10.25
+kfk Kinnauri kfk 10.32
+kfp Korwa kfp 11.87
+khg Khams Tibetan khg 6.38
+khw Khowar khw 15.55
+kj Kuanyama kua 9.88
+kjc Coastal Konjo kjc 10.18
+kjk Highland Konjo kjk 10.21
+kk Kazakh kaz 1537.29
+kln Kalenjin kln 40.42
+kls Kalasha kls 9.11
+km Khmer khm 7.1
+kmr Northern Kurdish kmr 69.59
+kmy Koma kmy 10.28
+kn Kannada kan 128.06
+kna Dera (Nigeria) kna 11.91
+knn Konkani knn 112.83
+ko Korean kor 8609.28
+kol Kol (Papua New Guinea) kol 9.95
+koo Konzo koo 13.23
+kpo Ikposo kpo 7.83
+kqo Eastern Krahn kqo 9.28
+ks Kashmiri kas 110.42
+ksd Kuanua ksd 9.91
+ksf Bafia ksf 16.43
+kto Kuot kto 9.77
+kuh Kushi kuh 10.35
+kvx Parkari Koli kvx 11.04
+kw Cornish cor 12.15
+kwm Kwambi kwm 9.9
+kxp Wadiyara Koli kxp 20.0
+ky Kirghiz kir 46.63
+kyx Rapoisi kyx 9.17
+lag Rangi lag 9.47
+lb Luxembourgish ltz 8.46
+lcm Tungag lcm 9.77
+ldb Dũya ldb 11.31
+lg Ganda lug 447.82
+lij Ligurian lij 15.97
+lir Liberian English lir 10.26
+lkb Kabras lkb 9.99
+lla Lala-Roba lla 10.38
+ln Lingala lin 17.99
+lnu Longuda lnu 10.46
+lo Lao lao 7.63
+loa Loloda loa 9.31
+lrk Loarki lrk 10.5
+lss Lasi lss 6.53
+lt Lithuanian lit 2629.45
+ltg Latgalian ltg 27.23
+lto Tsotso lto 9.77
+lua Luba-Lulua lua 8.47
+luo Luo luo 36.17
+lus Lushai lus 20.24
+lv Latvian lav 1441.58
+lwg Wanga lwg 9.36
+mab Yutanduchi Mixtec mab 9.26
+maf Mafa maf 9.97
+mai Maithili mai 131.37
+mau Huautla Mazatec mau 6.39
+max North Moluccan Malay max 9.43
+mbo Mbo (Cameroon) mbo 9.51
+mcf Matsés mcf 9.61
+mcn Masana mcn 10.09
+mcx Mpiemo mcx 9.88
+mdd Mbum mdd 9.82
+mde Maba (Chad) mde 9.5
+mdf Moksha mdf 0.47
+mek Mekeo mek 9.18
+mer Meru mer 9.89
+meu Motu meu 9.88
+mfm Marghi South mfm 10.05
+mfn Cross River Mbembe mfn 10.03
+mfo Mbe mfo 10.24
+mfv Mandjak mfv 9.55
+mgg Mpumpong mgg 4.94
+mgi Lijili mgi 10.89
+mhk Mungaka mhk 7.53
+mhr Eastern Mari mhr 272.31
+mi Maori mri 18.02
+mig San Miguel El Grande Mixtec mig 9.66
+miu Cacaloxtepec Mixtec miu 9.18
+mk Macedonian mkd 27.21
+mkf Miya mkf 10.16
+mki Dhatki mki 8.83
+ml Malayalam mal 166.57
+mlq Western Maninkakan mlq 9.83
+mn Mongolian mon 269.08
+mne Naba mne 10.37
+mni Manipuri mni 44.46
+mqy Manggarai mqy 10.5
+mr Marathi mar 156.71
+mrj Western Mari mrj 32.26
+mrr Maria (India) mrr 11.0
+mrt Marghi Central mrt 10.36
+ms Malay msa 9.57
+mse Musey mse 7.21
+msh Masikoro Malagasy msh 14.16
+msw Mansoanka msw 9.32
+mt Maltese mlt 630.29
+mtr Mewari mtr 10.58
+mtu Tututepec Mixtec mtu 10.13
+mtx Tidaá Mixtec mtx 9.09
+mua Mundang mua 9.2
+mug Musgu mug 4.74
+mui Musi mui 10.52
+mve Marwari (Pakistan) mve 9.96
+mvy Indus Kohistani mvy 21.64
+mxs Huitepec Mixtec mxs 9.64
+mxu Mada (Cameroon) mxu 12.0
+mxy Southeastern Nochixtlán Mixtec mxy 9.48
+my Burmese mya 12.14
+myv Erzya myv 3.1
+mzl Mazatlán Mixe mzl 10.05
+nal Nalik nal 10.33
+nan Min Nan Chinese nan 17.55
+nap Neapolitan nap 9.97
+nb Norwegian Bokmål nob 12.7
+nbh Ngamo nbh 10.04
+ncf Notsi ncf 9.84
+nco Sibe nco 9.96
+ncx Central Puebla Nahuatl ncx 9.86
+ndi Samba Leko ndi 11.27
+ng Ndonga ndo 9.08
+ngi Ngizim ngi 10.06
+nhg Tetelcingo Nahuatl nhg 8.92
+nhi Zacatlán-Ahuacatlán-Tepetzintla Nahuatl nhi 0.05
+nhn Central Nahuatl nhn 9.51
+nhq Huaxcaleca Nahuatl nhq 5.07
+nja Nzanyi nja 10.02
+nl Dutch nld 2264.13
+nla Ngombale nla 8.79
+nlv Orizaba Nahuatl nlv 11.42
+nmg Kwasio nmg 10.39
+nmz Nawdm nmz 6.3
+nn Norwegian Nynorsk nno 1.54
+nnh Ngiemboon nnh 16.15
+no Norwegian nor 3849.8
+noe Nimadi noe 11.12
+npi Nepali npi 171.5
+nso Pedi nso 12.64
+ny Chichewa nya 10.8
+nyu Nyungwe nyu 8.98
+oc Occitan oci 16.8
+odk Od odk 20.26
+odu Odual odu 10.57
+ogo Khana ogo 10.51
+om Oromo orm 6.6
+orc Orma orc 22.01
+oru Ormuri oru 16.74
+ory Odia ory 144.81
+os Iron Ossetic oss 1.38
+pa Panjabi pan 147.37
+pbs Central Pame pbs 9.69
+pbt Southern Pashto pbt 11.6
+pbu Northern Pashto pbu 11.03
+pcm Nigerian Pidgin pcm 11.04
+pex Petats pex 10.2
+phl Phalura phl 20.69
+phr Pahari-Potwari phr 24.03
+pip Pero pip 9.85
+piy Piya-Kwonci piy 10.38
+pko Pökoot pko 10.4
+pl Polish pol 911.68
+plk Kohistani Shina plk 12.75
+plt Plateau Malagasy plt 19.39
+pmq Northern Pame pmq 10.24
+pms Piemontese pms 16.01
+pmy Papuan Malay pmy 10.17
+pnb Western Panjabi pnb 10.0
+poc Poqomam poc 9.63
+poe San Juan Atzingo Popoloca poe 10.01
+pow San Felipe Otlaltepec Popoloca pow 8.84
+prq Ashéninka Perené prq 7.16
+ps Pushto pus 88.62
+pst Central Pashto pst 11.4
+pt Portuguese por 16855.05
+pua Western Highland Purepecha pua 10.17
+pwn Paiwan pwn 13.76
+qug Chimborazo Highland Quichua qug 10.12
+qum Sipacapense qum 9.37
+qup Southern Pastaza Quechua qup 11.13
+qur Yanahuanca Pasco Quechua qur 9.95
+qus Santiago del Estero Quichua qus 9.55
+quv Sacapulteco quv 8.9
+qux Yauyos Quechua qux 9.35
+quy Ayacucho Quechua quy 0.05
+qva Ambo-Pasco Quechua qva 9.59
+qvi Imbabura Highland Quichua qvi 11.0
+qvj Loja Highland Quichua qvj 10.59
+qvl Cajatambo North Lima Quechua qvl 9.95
+qwa Corongo Ancash Quechua qwa 9.72
+qws Sihuas Ancash Quechua qws 10.18
+qxa Chiquián Ancash Quechua qxa 9.99
+qxp Puno Quechua qxp 9.81
+qxt Santa Ana de Tusi Pasco Quechua qxt 10.05
+qxu Arequipa-La Unión Quechua qxu 10.12
+qxw Jauja Wanca Quechua qxw 11.42
+rag Logooli rag 9.39
+rm Romansh roh 9.21
+ro Romanian ron 70.23
+rob Tae' rob 9.02
+rof Rombo rof 18.9
+roo Rotokas roo 9.07
+rth Ratahan rth 9.34
+ru Russian rus 20338.5
+rup Macedo-Romanian rup 0.02
+rw Kinyarwanda kin 2021.66
+sa Sanskrit san 84.44
+sah Yakut sah 16.08
+sat Santali sat 98.37
+sau Saleman sau 10.53
+say Saya say 10.02
+sbn Sindhi Bhil sbn 10.53
+sc Sardinian srd 2.77
+scl Shina scl 9.84
+scn Sicilian scn 13.35
+sd Sindhi snd 46.27
+sei Seri sei 9.81
+shu Chadian Arabic shu 2.29
+si Sinhala sin 11.98
+sip Sikkimese sip 10.07
+siw Siwai siw 10.47
+sjr Siar-Lak sjr 9.87
+sk Slovak slk 2478.46
+skg Sakalava Malagasy skg 9.02
+skr Saraiki skr 4.13
+sl Slovenian slv 1172.61
+sn Shona sna 9.96
+snc Sinaugoro snc 10.38
+snk Soninke snk 10.04
+so Somali som 13.22
+sol Solos sol 9.95
+sps Saposa sps 9.81
+sq Albanian sqi 8.59
+sr Serbian srp 1855.33
+src Logudorese Sardinian src 10.67
+sro Campidanese Sardinian sro 10.16
+ssi Sansi ssi 10.47
+ste Liana-Seti ste 10.43
+sua Sulka sua 10.12
+sv Swedish swe 2453.14
+sva Svan sva 15.11
+sw Swahili swa 418.41
+szy Sakizaya szy 11.47
+ta Tamil tam 423.09
+tan Tangale tan 10.14
+tar Central Tarahumara tar 9.73
+tay Atayal tay 7.02
+tbf Mandara tbf 10.01
+tcf Malinaltepec Me'phaa tcf 9.04
+tcy Tulu tcy 11.72
+tdn Tondano tdn 9.14
+tdx Tandroy-Mahafaly Malagasy tdx 3.81
+te Telugu tel 230.21
+tg Tajik tgk 9.23
+tgc Tigak tgc 9.71
+th Thai tha 10499.77
+the Chitwania Tharu the 10.06
+thq Kochila Tharu thq 10.28
+thr Rana Tharu thr 9.99
+thv Tahaggart Tamahaq thv 4.25
+ti Tigrinya tir 0.08
+tig Tigre tig 7.49
+tio Teop tio 9.85
+tk Turkmen tuk 2.86
+tkg Tesaka Malagasy tkg 17.86
+tkt Kathoriya Tharu tkt 10.64
+tli Tlingit tli 0.41
+tlp Filomena Mata-Coahuitlán Totonac tlp 11.35
+tn Tswana tsn 4.24
+tok Toki Pona tok 13.51
+tpl Tlacoapa Me'phaa tpl 9.28
+tpz Tinputz tpz 9.33
+tqp Tomoip tqp 10.1
+tr Turkish tur 125.36
+trp Kok Borok trp 10.74
+trq San Martín Itunyoso Triqui trq 8.29
+trv Sediq trv 7.77
+trw Torwali trw 14.98
+tt Tatar tat 30.03
+ttj Tooro ttj 10.31
+ttr Tera ttr 9.89
+ttu Torau ttu 9.87
+tui Tupuri tui 9.26
+tul Tula tul 9.79
+tuq Tedaga tuq 10.0
+tuv Turkana tuv 10.17
+tuy Tugen tuy 8.79
+tvo Tidore tvo 10.31
+tvu Tunen tvu 9.85
+tw Twi twi 0.25
+twu Termanu twu 11.45
+txs Tonsea txs 9.32
+txy Tanosy Malagasy txy 12.07
+udl Wuzlam udl 9.23
+ug Uighur uig 428.77
+uk Ukrainian ukr 1851.97
+uki Kui (India) uki 10.77
+umb Umbundu umb 10.59
+ur Urdu urd 211.27
+ush Ushojo ush 6.36
+uz Uzbek uzb 115.28
+uzn Northern Uzbek uzn 15.23
+vai Vai vai 8.76
+var Huarijio var 9.28
+ver Mom Jango ver 10.93
+vi Vietnamese vie 8481.98
+vmc Juxtlahuaca Mixtec vmc 9.43
+vmj Ixtayutla Mixtec vmj 10.17
+vmm Mitlatongo Mixtec vmm 9.95
+vmp Soyaltepec Mazatec vmp 10.17
+vmz Mazatlán Mazatec vmz 9.82
+vot Votic vot 0.1
+vro Võro vro 15.66
+wbl Wakhi wbl 11.67
+wci Waci Gbe wci 8.02
+weo Wemale weo 9.09
+wes Cameroon Pidgin wes 10.06
+wja Waja wja 10.22
+wji Warji wji 11.39
+wo Wolof wol 8.71
+wof Gambian Wolof wof 9.46
+xh Xhosa xho 13.35
+xhe Khetrani xhe 9.4
+xka Kalkoti xka 8.0
+xmf Mingrelian xmf 11.47
+xmv Antankarana Malagasy xmv 17.9
+xmw Tsimihety Malagasy xmw 11.53
+xpe Liberia Kpelle xpe 9.5
+xti Sinicahua Mixtec xti 9.5
+xtu Cuyamecalco Mixtec xtu 9.4
+yaq Yaqui yaq 9.93
+yav Yangben yav 8.7
+yay Agwagwune yay 8.26
+ydd Eastern Yiddish ydd 18.43
+ydg Yidgha ydg 9.89
+yer Tarok yer 10.08
+yes Nyankpa yes 10.26
+yi Yiddish yid 1.81
+yo Yoruba yor 15.66
+yue Cantonese yue 13302.38
+zga Kinga zga 9.5
+zgh Standard Moroccan Tamazight zgh 1.19
+zh Chinese cmn 111343.3
+zoc Copainalá Zoque zoc 10.07
+zoh Chimalapa Zoque zoh 9.35
+zor Rayón Zoque zor 9.04
+zpv Chichicapan Zapotec zpv 9.85
+zpy Mazaltepec Zapotec zpy 9.47
+ztg Xanaguía Zapotec ztg 9.86
+ztn Santa Catarina Albarradas Zapotec ztn 10.02
+ztp Loxicha Zapotec ztp 9.62
+zts Tilquiapan Zapotec zts 9.33
+ztu Güilá Zapotec ztu 9.17
+zu Zulu zul 14.83
+zza Zaza zza 1.52
diff --git a/docs/languages.md b/docs/languages.md
new file mode 100644
index 00000000..ac4cfedc
--- /dev/null
+++ b/docs/languages.md
@@ -0,0 +1,659 @@
+# Supported Languages
+
+OmniVoice supports **646 languages** with a total of **581k hours** of training data.
+
+The table below lists each language with its OmniVoice language ID,
+ISO 639-3 code, and training data duration (hours).
+
+| # | Language | OmniVoice ID | ISO 639-3 | Duration (h) |
+|--:|----------|:------------:|:---------:|:------------:|
+| 1 | Abadi | kbt | kbt | 9.73 |
+| 2 | Abkhazian | ab | abk | 57.27 |
+| 3 | Abron | abr | abr | 9.22 |
+| 4 | Abua | abn | abn | 10.27 |
+| 5 | Adamawa Fulfulde | fub | fub | 13.12 |
+| 6 | Adyghe | ady | ady | 32.6 |
+| 7 | Afade | aal | aal | 10.19 |
+| 8 | Afrikaans | af | afr | 4.4 |
+| 9 | Agwagwune | yay | yay | 8.26 |
+| 10 | Aja (Benin) | ajg | ajg | 5.63 |
+| 11 | Akebu | keu | keu | 9.1 |
+| 12 | Alago | ala | ala | 11.04 |
+| 13 | Albanian | sq | sqi | 8.59 |
+| 14 | Algerian Arabic | arq | arq | 9.64 |
+| 15 | Algerian Saharan Arabic | aao | aao | 2.02 |
+| 16 | Ambo-Pasco Quechua | qva | qva | 9.59 |
+| 17 | Ambonese Malay | abs | abs | 10.03 |
+| 18 | Amdo Tibetan | adx | adx | 56.94 |
+| 19 | Amharic | am | amh | 12.83 |
+| 20 | Anaang | anw | anw | 9.65 |
+| 21 | Angika | anp | anp | 10.65 |
+| 22 | Antankarana Malagasy | xmv | xmv | 17.9 |
+| 23 | Aragonese | an | arg | 16.4 |
+| 24 | Arbëreshë Albanian | aae | aae | 6.11 |
+| 25 | Arequipa-La Unión Quechua | qxu | qxu | 10.12 |
+| 26 | Armenian | hy | hye | 42.15 |
+| 27 | Ashe | ahs | ahs | 10.62 |
+| 28 | Ashéninka Perené | prq | prq | 7.16 |
+| 29 | Askopan | eiv | eiv | 10.44 |
+| 30 | Assamese | as | asm | 270.85 |
+| 31 | Asturian | ast | ast | 8.48 |
+| 32 | Atayal | tay | tay | 7.02 |
+| 33 | Awak | awo | awo | 10.22 |
+| 34 | Ayacucho Quechua | quy | quy | 0.05 |
+| 35 | Azerbaijani | az | aze | 9.84 |
+| 36 | Baatonum | bba | bba | 10.53 |
+| 37 | Bacama | bcy | bcy | 9.94 |
+| 38 | Bade | bde | bde | 9.89 |
+| 39 | Bafia | ksf | ksf | 16.43 |
+| 40 | Bafut | bfd | bfd | 9.03 |
+| 41 | Bagirmi Fulfulde | fui | fui | 15.04 |
+| 42 | Bago-Kusuntu | bqg | bqg | 8.86 |
+| 43 | Baharna Arabic | abv | abv | 10.41 |
+| 44 | Bakoko | bkh | bkh | 6.0 |
+| 45 | Balanta-Ganja | bjt | bjt | 9.41 |
+| 46 | Balti | bft | bft | 16.28 |
+| 47 | Bamenyam | bce | bce | 9.9 |
+| 48 | Bamun | bax | bax | 10.24 |
+| 49 | Bangwinji | bsj | bsj | 10.0 |
+| 50 | Banjar | bjn | bjn | 11.68 |
+| 51 | Bankon | abb | abb | 11.2 |
+| 52 | Baoulé | bci | bci | 10.21 |
+| 53 | Bara Malagasy | bhr | bhr | 12.14 |
+| 54 | Barok | bjk | bjk | 10.16 |
+| 55 | Basa (Cameroon) | bas | bas | 10.66 |
+| 56 | Basa (Nigeria) | bzw | bzw | 10.27 |
+| 57 | Bashkir | ba | bak | 249.1 |
+| 58 | Basque | eu | eus | 479.86 |
+| 59 | Batak Mandailing | btm | btm | 11.09 |
+| 60 | Batanga | bnm | bnm | 15.01 |
+| 61 | Bateri | btv | btv | 9.8 |
+| 62 | Bats | bbl | bbl | 11.22 |
+| 63 | Bayot | bda | bda | 9.47 |
+| 64 | Bebele | beb | beb | 7.52 |
+| 65 | Belarusian | be | bel | 1809.43 |
+| 66 | Bengali | bn | ben | 271.76 |
+| 67 | Betawi | bew | bew | 11.15 |
+| 68 | Bhili | bhb | bhb | 9.98 |
+| 69 | Bhojpuri | bho | bho | 10.05 |
+| 70 | Bilur | bxf | bxf | 10.84 |
+| 71 | Bima | bhp | bhp | 10.67 |
+| 72 | Bodo | brx | brx | 231.57 |
+| 73 | Boghom | bux | bux | 10.48 |
+| 74 | Bokyi | bky | bky | 9.85 |
+| 75 | Bomu | bmq | bmq | 10.68 |
+| 76 | Bondei | bou | bou | 9.98 |
+| 77 | Borgu Fulfulde | fue | fue | 20.1 |
+| 78 | Bosnian | bs | bos | 690.73 |
+| 79 | Brahui | brh | brh | 19.89 |
+| 80 | Braj | bra | bra | 10.68 |
+| 81 | Breton | br | bre | 25.48 |
+| 82 | Buduma | bdm | bdm | 10.17 |
+| 83 | Buginese | bug | bug | 11.09 |
+| 84 | Bukharic | bhh | bhh | 11.38 |
+| 85 | Bulgarian | bg | bul | 2190.76 |
+| 86 | Bulu (Cameroon) | bum | bum | 9.06 |
+| 87 | Bundeli | bns | bns | 10.88 |
+| 88 | Bunun | bnn | bnn | 9.26 |
+| 89 | Bura-Pabir | bwr | bwr | 10.4 |
+| 90 | Burak | bys | bys | 9.92 |
+| 91 | Burmese | my | mya | 12.14 |
+| 92 | Burushaski | bsk | bsk | 9.14 |
+| 93 | Cacaloxtepec Mixtec | miu | miu | 9.18 |
+| 94 | Cajatambo North Lima Quechua | qvl | qvl | 9.95 |
+| 95 | Cakfem-Mushere | cky | cky | 8.96 |
+| 96 | Cameroon Pidgin | wes | wes | 10.06 |
+| 97 | Campidanese Sardinian | sro | sro | 10.16 |
+| 98 | Cantonese | yue | yue | 13302.38 |
+| 99 | Catalan | ca | cat | 3358.6 |
+| 100 | Cebuano | ceb | ceb | 12.17 |
+| 101 | Cen | cen | cen | 9.85 |
+| 102 | Central Kurdish | ckb | ckb | 137.52 |
+| 103 | Central Nahuatl | nhn | nhn | 9.51 |
+| 104 | Central Pame | pbs | pbs | 9.69 |
+| 105 | Central Pashto | pst | pst | 11.4 |
+| 106 | Central Puebla Nahuatl | ncx | ncx | 9.86 |
+| 107 | Central Tarahumara | tar | tar | 9.73 |
+| 108 | Central Yupik | esu | esu | 2.18 |
+| 109 | Central-Eastern Niger Fulfulde | fuq | fuq | 9.28 |
+| 110 | Chadian Arabic | shu | shu | 2.29 |
+| 111 | Chichewa | ny | nya | 10.8 |
+| 112 | Chichicapan Zapotec | zpv | zpv | 9.85 |
+| 113 | Chiga | cgg | cgg | 10.84 |
+| 114 | Chimalapa Zoque | zoh | zoh | 9.35 |
+| 115 | Chimborazo Highland Quichua | qug | qug | 10.12 |
+| 116 | Chinese | zh | cmn | 111343.3 |
+| 117 | Chiquián Ancash Quechua | qxa | qxa | 9.99 |
+| 118 | Chitwania Tharu | the | the | 10.06 |
+| 119 | Chokwe | cjk | cjk | 11.01 |
+| 120 | Chuvash | cv | chv | 23.96 |
+| 121 | Cibak | ckl | ckl | 10.91 |
+| 122 | Coastal Konjo | kjc | kjc | 10.18 |
+| 123 | Copainalá Zoque | zoc | zoc | 10.07 |
+| 124 | Cornish | kw | cor | 12.15 |
+| 125 | Corongo Ancash Quechua | qwa | qwa | 9.72 |
+| 126 | Croatian | hr | hrv | 2795.31 |
+| 127 | Cross River Mbembe | mfn | mfn | 10.03 |
+| 128 | Cuyamecalco Mixtec | xtu | xtu | 9.4 |
+| 129 | Czech | cs | ces | 148.13 |
+| 130 | Dadiya | dbd | dbd | 9.61 |
+| 131 | Dagbani | dag | dag | 10.14 |
+| 132 | Dameli | dml | dml | 9.18 |
+| 133 | Danish | da | dan | 1665.98 |
+| 134 | Dargwa | dar | dar | 1.22 |
+| 135 | Dazaga | dzg | dzg | 9.96 |
+| 136 | Deccan | dcc | dcc | 10.38 |
+| 137 | Degema | deg | deg | 11.07 |
+| 138 | Dera (Nigeria) | kna | kna | 11.91 |
+| 139 | Dghwede | dgh | dgh | 9.95 |
+| 140 | Dhatki | mki | mki | 8.83 |
+| 141 | Dhivehi | dv | div | 38.61 |
+| 142 | Dhofari Arabic | adf | adf | 0.31 |
+| 143 | Dijim-Bwilim | cfa | cfa | 10.32 |
+| 144 | Dogri | dgo | dgo | 117.04 |
+| 145 | Domaaki | dmk | dmk | 6.38 |
+| 146 | Dotyali | dty | dty | 10.85 |
+| 147 | Duala | dua | dua | 12.13 |
+| 148 | Dutch | nl | nld | 2264.13 |
+| 149 | Dũya | ldb | ldb | 11.31 |
+| 150 | Dyula | dyu | dyu | 0.34 |
+| 151 | Eastern Balochi | bgp | bgp | 10.98 |
+| 152 | Eastern Bolivian Guaraní | gui | gui | 22.72 |
+| 153 | Eastern Egyptian Bedawi Arabic | avl | avl | 1.86 |
+| 154 | Eastern Krahn | kqo | kqo | 9.28 |
+| 155 | Eastern Mari | mhr | mhr | 272.31 |
+| 156 | Eastern Yiddish | ydd | ydd | 18.43 |
+| 157 | Ebrié | ebr | ebr | 1.5 |
+| 158 | Eggon | ego | ego | 9.95 |
+| 159 | Egyptian Arabic | arz | arz | 23.23 |
+| 160 | Ejagham | etu | etu | 10.3 |
+| 161 | Eleme | elm | elm | 11.27 |
+| 162 | Eloyi | afo | afo | 11.21 |
+| 163 | Embu | ebu | ebu | 9.81 |
+| 164 | English | en | eng | 206061.1 |
+| 165 | Erzya | myv | myv | 3.1 |
+| 166 | Esan | ish | ish | 10.05 |
+| 167 | Esperanto | eo | epo | 1396.64 |
+| 168 | Estonian | et | est | 960.37 |
+| 169 | Eton (Cameroon) | eto | eto | 7.43 |
+| 170 | Ewondo | ewo | ewo | 12.71 |
+| 171 | Extremaduran | ext | ext | 13.59 |
+| 172 | Fang (Equatorial Guinea) | fan | fan | 3.51 |
+| 173 | Fanti | fat | fat | 11.38 |
+| 174 | Farefare | gur | gur | 9.24 |
+| 175 | Fe'fe' | fmp | fmp | 9.86 |
+| 176 | Filipino | fil | fil | 7.71 |
+| 177 | Filomena Mata-Coahuitlán Totonac | tlp | tlp | 11.35 |
+| 178 | Finnish | fi | fin | 468.62 |
+| 179 | Fipa | fip | fip | 10.55 |
+| 180 | French | fr | fra | 23675.32 |
+| 181 | Fulah | ff | ful | 13.84 |
+| 182 | Galician | gl | glg | 208.81 |
+| 183 | Gambian Wolof | wof | wof | 9.46 |
+| 184 | Ganda | lg | lug | 447.82 |
+| 185 | Garhwali | gbm | gbm | 19.14 |
+| 186 | Gawar-Bati | gwt | gwt | 12.16 |
+| 187 | Gawri | gwc | gwc | 10.83 |
+| 188 | Gbagyi | gbr | gbr | 12.12 |
+| 189 | Gbari | gby | gby | 12.59 |
+| 190 | Geji | gyz | gyz | 10.49 |
+| 191 | Gen | gej | gej | 5.39 |
+| 192 | Georgian | ka | kat | 156.96 |
+| 193 | German | de | deu | 21927.13 |
+| 194 | Geser-Gorom | ges | ges | 10.08 |
+| 195 | Gheg Albanian | aln | aln | 3.92 |
+| 196 | Ghomálá' | bbj | bbj | 7.32 |
+| 197 | Gidar | gid | gid | 10.06 |
+| 198 | Glavda | glw | glw | 10.51 |
+| 199 | Goan Konkani | gom | gom | 9.82 |
+| 200 | Goaria | gig | gig | 9.41 |
+| 201 | Goemai | ank | ank | 10.0 |
+| 202 | Gola | gol | gol | 9.26 |
+| 203 | Greek | el | ell | 2412.54 |
+| 204 | Guarani | gn | grn | 4.06 |
+| 205 | Guduf-Gava | gdf | gdf | 12.21 |
+| 206 | Guerrero Amuzgo | amu | amu | 10.1 |
+| 207 | Gujarati | gu | guj | 91.18 |
+| 208 | Gujari | gju | gju | 8.66 |
+| 209 | Gulf Arabic | afb | afb | 98.55 |
+| 210 | Gurgula | ggg | ggg | 7.12 |
+| 211 | Gusii | guz | guz | 9.5 |
+| 212 | Gusilay | gsl | gsl | 10.0 |
+| 213 | Gweno | gwe | gwe | 8.87 |
+| 214 | Güilá Zapotec | ztu | ztu | 9.17 |
+| 215 | Hadothi | hoj | hoj | 10.08 |
+| 216 | Hahon | hah | hah | 9.64 |
+| 217 | Haitian | ht | hat | 0.04 |
+| 218 | Hakha Chin | cnh | cnh | 2.24 |
+| 219 | Hakö | hao | hao | 8.56 |
+| 220 | Halia | hla | hla | 9.86 |
+| 221 | Hausa | ha | hau | 17.75 |
+| 222 | Hawaiian | haw | haw | 11.79 |
+| 223 | Hazaragi | haz | haz | 9.69 |
+| 224 | Hebrew | he | heb | 13.4 |
+| 225 | Hemba | hem | hem | 9.53 |
+| 226 | Herero | hz | her | 9.59 |
+| 227 | Highland Konjo | kjk | kjk | 10.21 |
+| 228 | Hijazi Arabic | acw | acw | 22.32 |
+| 229 | Hindi | hi | hin | 117.17 |
+| 230 | Huarijio | var | var | 9.28 |
+| 231 | Huautla Mazatec | mau | mau | 6.39 |
+| 232 | Huaxcaleca Nahuatl | nhq | nhq | 5.07 |
+| 233 | Huba | hbb | hbb | 10.7 |
+| 234 | Huitepec Mixtec | mxs | mxs | 9.64 |
+| 235 | Hula | hul | hul | 10.33 |
+| 236 | Hungarian | hu | hun | 255.83 |
+| 237 | Hunjara-Kaina Ke | hkk | hkk | 8.69 |
+| 238 | Hwana | hwo | hwo | 11.23 |
+| 239 | Ibibio | ibb | ibb | 7.38 |
+| 240 | Icelandic | is | isl | 647.29 |
+| 241 | Idakho-Isukha-Tiriki | ida | ida | 9.31 |
+| 242 | Idoma | idu | idu | 11.16 |
+| 243 | Igbo | ig | ibo | 13.69 |
+| 244 | Igo | ahl | ahl | 9.22 |
+| 245 | Ikposo | kpo | kpo | 7.83 |
+| 246 | Ikwere | ikw | ikw | 10.0 |
+| 247 | Imbabura Highland Quichua | qvi | qvi | 11.0 |
+| 248 | Indonesian | id | ind | 6327.87 |
+| 249 | Indus Kohistani | mvy | mvy | 21.64 |
+| 250 | Interlingua (International Auxiliary Language Association) | ia | ina | 13.48 |
+| 251 | Inupiaq | ik | ipk | 2.11 |
+| 252 | Irish | ga | gle | 21.4 |
+| 253 | Iron Ossetic | os | oss | 1.38 |
+| 254 | Isekiri | its | its | 11.85 |
+| 255 | Isoko | iso | iso | 10.33 |
+| 256 | Italian | it | ita | 9402.46 |
+| 257 | Ito | itw | itw | 9.19 |
+| 258 | Itzá | itz | itz | 7.08 |
+| 259 | Ixtayutla Mixtec | vmj | vmj | 10.17 |
+| 260 | Izon | ijc | ijc | 9.95 |
+| 261 | Jambi Malay | jax | jax | 10.29 |
+| 262 | Japanese | ja | jpn | 36914.4 |
+| 263 | Jaqaru | jqr | jqr | 9.32 |
+| 264 | Jauja Wanca Quechua | qxw | qxw | 11.42 |
+| 265 | Jaunsari | jns | jns | 11.25 |
+| 266 | Javanese | jv | jav | 11.19 |
+| 267 | Jiba | juo | juo | 10.43 |
+| 268 | Jju | kaj | kaj | 10.16 |
+| 269 | Judeo-Moroccan Arabic | aju | aju | 7.21 |
+| 270 | Juxtlahuaca Mixtec | vmc | vmc | 9.43 |
+| 271 | Kabardian | kbd | kbd | 108.35 |
+| 272 | Kabras | lkb | lkb | 9.99 |
+| 273 | Kabuverdianu | kea | kea | 10.51 |
+| 274 | Kabyle | kab | kab | 529.52 |
+| 275 | Kachi Koli | gjk | gjk | 20.83 |
+| 276 | Kairak | ckr | ckr | 10.51 |
+| 277 | Kalabari | ijn | ijn | 11.04 |
+| 278 | Kalasha | kls | kls | 9.11 |
+| 279 | Kalenjin | kln | kln | 40.42 |
+| 280 | Kalkoti | xka | xka | 8.0 |
+| 281 | Kamba | kam | kam | 14.72 |
+| 282 | Kamo | kcq | kcq | 10.49 |
+| 283 | Kanauji | bjj | bjj | 11.01 |
+| 284 | Kanembu | kbl | kbl | 10.19 |
+| 285 | Kannada | kn | kan | 128.06 |
+| 286 | Karekare | kai | kai | 10.52 |
+| 287 | Kashmiri | ks | kas | 110.42 |
+| 288 | Kathoriya Tharu | tkt | tkt | 10.64 |
+| 289 | Kati | bsh | bsh | 8.77 |
+| 290 | Kazakh | kk | kaz | 1537.29 |
+| 291 | Keiyo | eyo | eyo | 9.24 |
+| 292 | Khams Tibetan | khg | khg | 6.38 |
+| 293 | Khana | ogo | ogo | 10.51 |
+| 294 | Khetrani | xhe | xhe | 9.4 |
+| 295 | Khmer | km | khm | 7.1 |
+| 296 | Khowar | khw | khw | 15.55 |
+| 297 | Kinga | zga | zga | 9.5 |
+| 298 | Kinnauri | kfk | kfk | 10.32 |
+| 299 | Kinyarwanda | rw | kin | 2021.66 |
+| 300 | Kirghiz | ky | kir | 46.63 |
+| 301 | Kirya-Konzəl | fkk | fkk | 9.98 |
+| 302 | Kochila Tharu | thq | thq | 10.28 |
+| 303 | Kohistani Shina | plk | plk | 12.75 |
+| 304 | Kohumono | bcs | bcs | 10.45 |
+| 305 | Kok Borok | trp | trp | 10.74 |
+| 306 | Kol (Papua New Guinea) | kol | kol | 9.95 |
+| 307 | Kom (Cameroon) | bkm | bkm | 10.76 |
+| 308 | Koma | kmy | kmy | 10.28 |
+| 309 | Konkani | knn | knn | 112.83 |
+| 310 | Konzo | koo | koo | 13.23 |
+| 311 | Korean | ko | kor | 8609.28 |
+| 312 | Korwa | kfp | kfp | 11.87 |
+| 313 | Kota (India) | kfe | kfe | 10.25 |
+| 314 | Koti | eko | eko | 8.15 |
+| 315 | Kuanua | ksd | ksd | 9.91 |
+| 316 | Kuanyama | kj | kua | 9.88 |
+| 317 | Kui (India) | uki | uki | 10.77 |
+| 318 | Kulung (Nigeria) | bbu | bbu | 10.39 |
+| 319 | Kuot | kto | kto | 9.77 |
+| 320 | Kushi | kuh | kuh | 10.35 |
+| 321 | Kwambi | kwm | kwm | 9.9 |
+| 322 | Kwasio | nmg | nmg | 10.39 |
+| 323 | Lala-Roba | lla | lla | 10.38 |
+| 324 | Lamang | hia | hia | 11.07 |
+| 325 | Lao | lo | lao | 7.63 |
+| 326 | Larike-Wakasihu | alo | alo | 9.97 |
+| 327 | Lasi | lss | lss | 6.53 |
+| 328 | Latgalian | ltg | ltg | 27.23 |
+| 329 | Latvian | lv | lav | 1441.58 |
+| 330 | Levantine Arabic | apc | apc | 15.65 |
+| 331 | Liana-Seti | ste | ste | 10.43 |
+| 332 | Liberia Kpelle | xpe | xpe | 9.5 |
+| 333 | Liberian English | lir | lir | 10.26 |
+| 334 | Libyan Arabic | ayl | ayl | 20.13 |
+| 335 | Ligurian | lij | lij | 15.97 |
+| 336 | Lijili | mgi | mgi | 10.89 |
+| 337 | Lingala | ln | lin | 17.99 |
+| 338 | Lithuanian | lt | lit | 2629.45 |
+| 339 | Loarki | lrk | lrk | 10.5 |
+| 340 | Logooli | rag | rag | 9.39 |
+| 341 | Logudorese Sardinian | src | src | 10.67 |
+| 342 | Loja Highland Quichua | qvj | qvj | 10.59 |
+| 343 | Loloda | loa | loa | 9.31 |
+| 344 | Longuda | lnu | lnu | 10.46 |
+| 345 | Loxicha Zapotec | ztp | ztp | 9.62 |
+| 346 | Luba-Lulua | lua | lua | 8.47 |
+| 347 | Luo | luo | luo | 36.17 |
+| 348 | Lushai | lus | lus | 20.24 |
+| 349 | Luxembourgish | lb | ltz | 8.46 |
+| 350 | Maasina Fulfulde | ffm | ffm | 10.46 |
+| 351 | Maba (Chad) | mde | mde | 9.5 |
+| 352 | Macedo-Romanian | rup | rup | 0.02 |
+| 353 | Macedonian | mk | mkd | 27.21 |
+| 354 | Mada (Cameroon) | mxu | mxu | 12.0 |
+| 355 | Mafa | maf | maf | 9.97 |
+| 356 | Maithili | mai | mai | 131.37 |
+| 357 | Malay | ms | msa | 9.57 |
+| 358 | Malayalam | ml | mal | 166.57 |
+| 359 | Mali | gcc | gcc | 9.87 |
+| 360 | Malinaltepec Me'phaa | tcf | tcf | 9.04 |
+| 361 | Maltese | mt | mlt | 630.29 |
+| 362 | Mandara | tbf | tbf | 10.01 |
+| 363 | Mandjak | mfv | mfv | 9.55 |
+| 364 | Manggarai | mqy | mqy | 10.5 |
+| 365 | Manipuri | mni | mni | 44.46 |
+| 366 | Mansoanka | msw | msw | 9.32 |
+| 367 | Manx | gv | glv | 10.07 |
+| 368 | Maori | mi | mri | 18.02 |
+| 369 | Marathi | mr | mar | 156.71 |
+| 370 | Marghi Central | mrt | mrt | 10.36 |
+| 371 | Marghi South | mfm | mfm | 10.05 |
+| 372 | Maria (India) | mrr | mrr | 11.0 |
+| 373 | Marwari (Pakistan) | mve | mve | 9.96 |
+| 374 | Masana | mcn | mcn | 10.09 |
+| 375 | Masikoro Malagasy | msh | msh | 14.16 |
+| 376 | Matsés | mcf | mcf | 9.61 |
+| 377 | Mazaltepec Zapotec | zpy | zpy | 9.47 |
+| 378 | Mazatlán Mazatec | vmz | vmz | 9.82 |
+| 379 | Mazatlán Mixe | mzl | mzl | 10.05 |
+| 380 | Mbe | mfo | mfo | 10.24 |
+| 381 | Mbo (Cameroon) | mbo | mbo | 9.51 |
+| 382 | Mbum | mdd | mdd | 9.82 |
+| 383 | Medumba | byv | byv | 10.95 |
+| 384 | Mekeo | mek | mek | 9.18 |
+| 385 | Meru | mer | mer | 9.89 |
+| 386 | Mesopotamian Arabic | acm | acm | 3.78 |
+| 387 | Mewari | mtr | mtr | 10.58 |
+| 388 | Min Nan Chinese | nan | nan | 17.55 |
+| 389 | Mingrelian | xmf | xmf | 11.47 |
+| 390 | Mitlatongo Mixtec | vmm | vmm | 9.95 |
+| 391 | Miya | mkf | mkf | 10.16 |
+| 392 | Mokpwe | bri | bri | 7.53 |
+| 393 | Moksha | mdf | mdf | 0.47 |
+| 394 | Mom Jango | ver | ver | 10.93 |
+| 395 | Mongolian | mn | mon | 269.08 |
+| 396 | Moroccan Arabic | ary | ary | 104.67 |
+| 397 | Motu | meu | meu | 9.88 |
+| 398 | Mpiemo | mcx | mcx | 9.88 |
+| 399 | Mpumpong | mgg | mgg | 4.94 |
+| 400 | Mundang | mua | mua | 9.2 |
+| 401 | Mungaka | mhk | mhk | 7.53 |
+| 402 | Musey | mse | mse | 7.21 |
+| 403 | Musgu | mug | mug | 4.74 |
+| 404 | Musi | mui | mui | 10.52 |
+| 405 | Naba | mne | mne | 10.37 |
+| 406 | Najdi Arabic | ars | ars | 203.54 |
+| 407 | Nalik | nal | nal | 10.33 |
+| 408 | Nawdm | nmz | nmz | 6.3 |
+| 409 | Ndonga | ng | ndo | 9.08 |
+| 410 | Neapolitan | nap | nap | 9.97 |
+| 411 | Nepali | npi | npi | 171.5 |
+| 412 | Ngamo | nbh | nbh | 10.04 |
+| 413 | Ngas | anc | anc | 10.14 |
+| 414 | Ngiemboon | nnh | nnh | 16.15 |
+| 415 | Ngizim | ngi | ngi | 10.06 |
+| 416 | Ngomba | jgo | jgo | 10.15 |
+| 417 | Ngombale | nla | nla | 8.79 |
+| 418 | Nigerian Fulfulde | fuv | fuv | 9.97 |
+| 419 | Nigerian Pidgin | pcm | pcm | 11.04 |
+| 420 | Nimadi | noe | noe | 11.12 |
+| 421 | Nobiin | fia | fia | 9.96 |
+| 422 | North Mesopotamian Arabic | ayp | ayp | 10.92 |
+| 423 | North Moluccan Malay | max | max | 9.43 |
+| 424 | Northern Betsimisaraka Malagasy | bmm | bmm | 19.12 |
+| 425 | Northern Hindko | hno | hno | 20.04 |
+| 426 | Northern Kurdish | kmr | kmr | 69.59 |
+| 427 | Northern Pame | pmq | pmq | 10.24 |
+| 428 | Northern Pashto | pbu | pbu | 11.03 |
+| 429 | Northern Uzbek | uzn | uzn | 15.23 |
+| 430 | Northwest Gbaya | gya | gya | 8.45 |
+| 431 | Norwegian | no | nor | 3849.8 |
+| 432 | Norwegian Bokmål | nb | nob | 12.7 |
+| 433 | Norwegian Nynorsk | nn | nno | 1.54 |
+| 434 | Notsi | ncf | ncf | 9.84 |
+| 435 | Nyankpa | yes | yes | 10.26 |
+| 436 | Nyungwe | nyu | nyu | 8.98 |
+| 437 | Nzanyi | nja | nja | 10.02 |
+| 438 | Nüpode Huitoto | hux | hux | 9.04 |
+| 439 | Occitan | oc | oci | 16.8 |
+| 440 | Od | odk | odk | 20.26 |
+| 441 | Odia | ory | ory | 144.81 |
+| 442 | Odual | odu | odu | 10.57 |
+| 443 | Omani Arabic | acx | acx | 22.03 |
+| 444 | Orizaba Nahuatl | nlv | nlv | 11.42 |
+| 445 | Orma | orc | orc | 22.01 |
+| 446 | Ormuri | oru | oru | 16.74 |
+| 447 | Oromo | om | orm | 6.6 |
+| 448 | Pahari-Potwari | phr | phr | 24.03 |
+| 449 | Paiwan | pwn | pwn | 13.76 |
+| 450 | Panjabi | pa | pan | 147.37 |
+| 451 | Papuan Malay | pmy | pmy | 10.17 |
+| 452 | Parkari Koli | kvx | kvx | 11.04 |
+| 453 | Pedi | nso | nso | 12.64 |
+| 454 | Pero | pip | pip | 9.85 |
+| 455 | Persian | fa | fas | 366.07 |
+| 456 | Petats | pex | pex | 10.2 |
+| 457 | Phalura | phl | phl | 20.69 |
+| 458 | Piemontese | pms | pms | 16.01 |
+| 459 | Piya-Kwonci | piy | piy | 10.38 |
+| 460 | Plateau Malagasy | plt | plt | 19.39 |
+| 461 | Polish | pl | pol | 911.68 |
+| 462 | Poqomam | poc | poc | 9.63 |
+| 463 | Portuguese | pt | por | 16855.05 |
+| 464 | Pulaar | fuc | fuc | 14.77 |
+| 465 | Pular | fuf | fuf | 13.77 |
+| 466 | Puno Quechua | qxp | qxp | 9.81 |
+| 467 | Pushto | ps | pus | 88.62 |
+| 468 | Pökoot | pko | pko | 10.4 |
+| 469 | Qaqet | byx | byx | 9.79 |
+| 470 | Quiotepec Chinantec | chq | chq | 9.76 |
+| 471 | Rana Tharu | thr | thr | 9.99 |
+| 472 | Rangi | lag | lag | 9.47 |
+| 473 | Rapoisi | kyx | kyx | 9.17 |
+| 474 | Ratahan | rth | rth | 9.34 |
+| 475 | Rayón Zoque | zor | zor | 9.04 |
+| 476 | Romanian | ro | ron | 70.23 |
+| 477 | Romansh | rm | roh | 9.21 |
+| 478 | Rombo | rof | rof | 18.9 |
+| 479 | Rotokas | roo | roo | 9.07 |
+| 480 | Rukai | dru | dru | 9.26 |
+| 481 | Russian | ru | rus | 20338.5 |
+| 482 | Sacapulteco | quv | quv | 8.9 |
+| 483 | Saidi Arabic | aec | aec | 9.28 |
+| 484 | Sakalava Malagasy | skg | skg | 9.02 |
+| 485 | Sakizaya | szy | szy | 11.47 |
+| 486 | Saleman | sau | sau | 10.53 |
+| 487 | Samba Daka | ccg | ccg | 10.11 |
+| 488 | Samba Leko | ndi | ndi | 11.27 |
+| 489 | San Felipe Otlaltepec Popoloca | pow | pow | 8.84 |
+| 490 | San Francisco Del Mar Huave | hue | hue | 9.45 |
+| 491 | San Juan Atzingo Popoloca | poe | poe | 10.01 |
+| 492 | San Martín Itunyoso Triqui | trq | trq | 8.29 |
+| 493 | San Miguel El Grande Mixtec | mig | mig | 9.66 |
+| 494 | Sansi | ssi | ssi | 10.47 |
+| 495 | Sanskrit | sa | san | 84.44 |
+| 496 | Santa Ana de Tusi Pasco Quechua | qxt | qxt | 10.05 |
+| 497 | Santa Catarina Albarradas Zapotec | ztn | ztn | 10.02 |
+| 498 | Santali | sat | sat | 98.37 |
+| 499 | Santiago del Estero Quichua | qus | qus | 9.55 |
+| 500 | Saposa | sps | sps | 9.81 |
+| 501 | Saraiki | skr | skr | 4.13 |
+| 502 | Sardinian | sc | srd | 2.77 |
+| 503 | Saya | say | say | 10.02 |
+| 504 | Sediq | trv | trv | 7.77 |
+| 505 | Serbian | sr | srp | 1855.33 |
+| 506 | Seri | sei | sei | 9.81 |
+| 507 | Shina | scl | scl | 9.84 |
+| 508 | Shona | sn | sna | 9.96 |
+| 509 | Siar-Lak | sjr | sjr | 9.87 |
+| 510 | Sibe | nco | nco | 9.96 |
+| 511 | Sicilian | scn | scn | 13.35 |
+| 512 | Sihuas Ancash Quechua | qws | qws | 10.18 |
+| 513 | Sikkimese | sip | sip | 10.07 |
+| 514 | Sinaugoro | snc | snc | 10.38 |
+| 515 | Sindhi | sd | snd | 46.27 |
+| 516 | Sindhi Bhil | sbn | sbn | 10.53 |
+| 517 | Sinhala | si | sin | 11.98 |
+| 518 | Sinicahua Mixtec | xti | xti | 9.5 |
+| 519 | Sipacapense | qum | qum | 9.37 |
+| 520 | Siwai | siw | siw | 10.47 |
+| 521 | Slovak | sk | slk | 2478.46 |
+| 522 | Slovenian | sl | slv | 1172.61 |
+| 523 | Solos | sol | sol | 9.95 |
+| 524 | Somali | so | som | 13.22 |
+| 525 | Soninke | snk | snk | 10.04 |
+| 526 | South Giziga | giz | giz | 10.03 |
+| 527 | South Ucayali Ashéninka | cpy | cpy | 9.15 |
+| 528 | Southeastern Nochixtlán Mixtec | mxy | mxy | 9.48 |
+| 529 | Southern Betsimisaraka Malagasy | bzc | bzc | 17.45 |
+| 530 | Southern Pashto | pbt | pbt | 11.6 |
+| 531 | Southern Pastaza Quechua | qup | qup | 11.13 |
+| 532 | Soyaltepec Mazatec | vmp | vmp | 10.17 |
+| 533 | Spanish | es | spa | 27559.74 |
+| 534 | Standard Arabic | arb | arb | 1483.53 |
+| 535 | Standard Moroccan Tamazight | zgh | zgh | 1.19 |
+| 536 | Sudanese Arabic | apd | apd | 9.93 |
+| 537 | Sulka | sua | sua | 10.12 |
+| 538 | Svan | sva | sva | 15.11 |
+| 539 | Swahili | sw | swa | 418.41 |
+| 540 | Swedish | sv | swe | 2453.14 |
+| 541 | Tae' | rob | rob | 9.02 |
+| 542 | Tahaggart Tamahaq | thv | thv | 4.25 |
+| 543 | Taita | dav | dav | 9.12 |
+| 544 | Tajik | tg | tgk | 9.23 |
+| 545 | Tamil | ta | tam | 423.09 |
+| 546 | Tandroy-Mahafaly Malagasy | tdx | tdx | 3.81 |
+| 547 | Tangale | tan | tan | 10.14 |
+| 548 | Tanosy Malagasy | txy | txy | 12.07 |
+| 549 | Tarok | yer | yer | 10.08 |
+| 550 | Tatar | tt | tat | 30.03 |
+| 551 | Tedaga | tuq | tuq | 10.0 |
+| 552 | Telugu | te | tel | 230.21 |
+| 553 | Tem | kdh | kdh | 4.07 |
+| 554 | Teop | tio | tio | 9.85 |
+| 555 | Tepeuxila Cuicatec | cux | cux | 7.83 |
+| 556 | Tepinapa Chinantec | cte | cte | 9.54 |
+| 557 | Tera | ttr | ttr | 9.89 |
+| 558 | Terei | buo | buo | 9.48 |
+| 559 | Termanu | twu | twu | 11.45 |
+| 560 | Tesaka Malagasy | tkg | tkg | 17.86 |
+| 561 | Tetelcingo Nahuatl | nhg | nhg | 8.92 |
+| 562 | Teutila Cuicatec | cut | cut | 8.04 |
+| 563 | Thai | th | tha | 10499.77 |
+| 564 | Tibetan | bo | bod | 82.27 |
+| 565 | Tidaá Mixtec | mtx | mtx | 9.09 |
+| 566 | Tidore | tvo | tvo | 10.31 |
+| 567 | Tigak | tgc | tgc | 9.71 |
+| 568 | Tigre | tig | tig | 7.49 |
+| 569 | Tigrinya | ti | tir | 0.08 |
+| 570 | Tilquiapan Zapotec | zts | zts | 9.33 |
+| 571 | Tinputz | tpz | tpz | 9.33 |
+| 572 | Tlacoapa Me'phaa | tpl | tpl | 9.28 |
+| 573 | Tlacoatzintepec Chinantec | ctl | ctl | 10.04 |
+| 574 | Tlingit | tli | tli | 0.41 |
+| 575 | Toki Pona | tok | tok | 13.51 |
+| 576 | Tomoip | tqp | tqp | 10.1 |
+| 577 | Tondano | tdn | tdn | 9.14 |
+| 578 | Tonsea | txs | txs | 9.32 |
+| 579 | Tooro | ttj | ttj | 10.31 |
+| 580 | Torau | ttu | ttu | 9.87 |
+| 581 | Torwali | trw | trw | 14.98 |
+| 582 | Tsimihety Malagasy | xmw | xmw | 11.53 |
+| 583 | Tsotso | lto | lto | 9.77 |
+| 584 | Tswana | tn | tsn | 4.24 |
+| 585 | Tugen | tuy | tuy | 8.79 |
+| 586 | Tuki | bag | bag | 10.97 |
+| 587 | Tula | tul | tul | 9.79 |
+| 588 | Tulu | tcy | tcy | 11.72 |
+| 589 | Tunen | tvu | tvu | 9.85 |
+| 590 | Tungag | lcm | lcm | 9.77 |
+| 591 | Tunisian Arabic | aeb | aeb | 21.63 |
+| 592 | Tupuri | tui | tui | 9.26 |
+| 593 | Turkana | tuv | tuv | 10.17 |
+| 594 | Turkish | tr | tur | 125.36 |
+| 595 | Turkmen | tk | tuk | 2.86 |
+| 596 | Tututepec Mixtec | mtu | mtu | 10.13 |
+| 597 | Twi | tw | twi | 0.25 |
+| 598 | Ubaghara | byc | byc | 11.11 |
+| 599 | Uighur | ug | uig | 428.77 |
+| 600 | Ukrainian | uk | ukr | 1851.97 |
+| 601 | Umbundu | umb | umb | 10.59 |
+| 602 | Upper Sorbian | hsb | hsb | 2.71 |
+| 603 | Urdu | ur | urd | 211.27 |
+| 604 | Ushojo | ush | ush | 6.36 |
+| 605 | Uzbek | uz | uzb | 115.28 |
+| 606 | Vai | vai | vai | 8.76 |
+| 607 | Vietnamese | vi | vie | 8481.98 |
+| 608 | Votic | vot | vot | 0.1 |
+| 609 | Võro | vro | vro | 15.66 |
+| 610 | Waci Gbe | wci | wci | 8.02 |
+| 611 | Wadiyara Koli | kxp | kxp | 20.0 |
+| 612 | Waja | wja | wja | 10.22 |
+| 613 | Wakhi | wbl | wbl | 11.67 |
+| 614 | Wanga | lwg | lwg | 9.36 |
+| 615 | Wapan | juk | juk | 10.22 |
+| 616 | Warji | wji | wji | 11.39 |
+| 617 | Welsh | cy | cym | 131.21 |
+| 618 | Wemale | weo | weo | 9.09 |
+| 619 | Western Frisian | fy | fry | 70.41 |
+| 620 | Western Highland Purepecha | pua | pua | 10.17 |
+| 621 | Western Juxtlahuaca Mixtec | jmx | jmx | 10.01 |
+| 622 | Western Maninkakan | mlq | mlq | 9.83 |
+| 623 | Western Mari | mrj | mrj | 32.26 |
+| 624 | Western Niger Fulfulde | fuh | fuh | 9.69 |
+| 625 | Western Panjabi | pnb | pnb | 10.0 |
+| 626 | Wolof | wo | wol | 8.71 |
+| 627 | Wuzlam | udl | udl | 9.23 |
+| 628 | Xanaguía Zapotec | ztg | ztg | 9.86 |
+| 629 | Xhosa | xh | xho | 13.35 |
+| 630 | Yace | ekr | ekr | 10.76 |
+| 631 | Yakut | sah | sah | 16.08 |
+| 632 | Yalahatan | jal | jal | 11.18 |
+| 633 | Yanahuanca Pasco Quechua | qur | qur | 9.95 |
+| 634 | Yangben | yav | yav | 8.7 |
+| 635 | Yaqui | yaq | yaq | 9.93 |
+| 636 | Yauyos Quechua | qux | qux | 9.35 |
+| 637 | Yekhee | ets | ets | 10.11 |
+| 638 | Yiddish | yi | yid | 1.81 |
+| 639 | Yidgha | ydg | ydg | 9.89 |
+| 640 | Yoruba | yo | yor | 15.66 |
+| 641 | Yutanduchi Mixtec | mab | mab | 9.26 |
+| 642 | Zacatlán-Ahuacatlán-Tepetzintla Nahuatl | nhi | nhi | 0.05 |
+| 643 | Zarma | dje | dje | 10.72 |
+| 644 | Zaza | zza | zza | 1.52 |
+| 645 | Zulu | zu | zul | 14.83 |
+| 646 | Ömie | aom | aom | 8.19 |
+
+*646 languages, 581k hours total.*
+
+Data source: [docs/lang_id_name_map.tsv](lang_id_name_map.tsv)
diff --git a/docs/training.md b/docs/training.md
new file mode 100644
index 00000000..03231712
--- /dev/null
+++ b/docs/training.md
@@ -0,0 +1,62 @@
+# Training
+
+## Training Config
+
+All training is controlled by a JSON training config file and a JSON data config file.
+
+See [examples/config/](../examples/config/) for ready-to-use configs.
+
+Training config file on Emilia is: [examples/config/train_config_emilia.json](../examples/config/train_config_emilia.json)
+
+Data config file for Emilia is: [examples/config/data_config_emilia.json](../examples/config/data_config_emilia.json)
+
+
+Key fields in training config file:
+
+| Field | Description | Default |
+|---|---|---|
+| `llm_name_or_path` | local LLM path or huggingface id | Qwen/Qwen3-0.6B |
+| `steps` | Total training steps | 300,000 |
+| `learning_rate` | Peak learning rate | 1e-4 |
+| `batch_tokens` | Tokens per batch on each GPU | 8192 |
+
+`output_dir` and `data_config` are passed via command line (see below).
+
+## Launching Training
+
+```bash
+accelerate launch \
+ --gpu_ids "0,1,2,3,4,5,6,7" \
+ --num_processes 8 \
+ -m omnivoice.cli.train \
+ --train_config config/train_config_emilia.json \
+ --data_config config/data_config_emilia.json \
+ --output_dir exp/omnivoice_emilia
+```
+
+## Resuming Training
+
+Set `resume_from_checkpoint` in your training config to resume from an existing checkpoint:
+
+```json
+{
+ "resume_from_checkpoint": "exp/omnivoice/checkpoint-100000"
+}
+```
+
+## Initializing from a Pretrained Model
+
+To start training from a pretrained OmniVoice checkpoint (for fine-tuning):
+
+```json
+{
+ "init_from_checkpoint": "exp/omnivoice/checkpoint-100000"
+}
+```
+
+## Monitoring
+
+Training logs to TensorBoard:
+```bash
+tensorboard --logdir exp/omnivoice_emilia/tensorboard
+```
diff --git a/docs/voice-design.md b/docs/voice-design.md
new file mode 100644
index 00000000..6df90120
--- /dev/null
+++ b/docs/voice-design.md
@@ -0,0 +1,128 @@
+# Voice Design
+
+Voice Design mode lets you describe the desired speaker through speaker attributes (`instruct` parameter) — no reference audio needed. The model
+generates a matching voice on the fly.
+
+## Quick Example
+
+```python
+import torch
+from omnivoice import OmniVoice
+
+model = OmniVoice.from_pretrained(
+ "k2-fsa/OmniVoice",
+ device_map="cuda:0",
+ dtype=torch.float16
+)
+
+audio = model.generate(
+ text="This is a test for voice design.",
+ instruct="female, young adult, high pitch, british accent",
+)
+```
+
+## How It Works
+
+The `instruct` parameter accepts a comma-separated string of speaker attributes.
+Each attribute belongs to a **category** (gender, age, pitch, style, accent,
+or dialect). Within a category, only one attribute may be selected at a time.
+Attributes from different categories can be freely combined.
+
+The model auto-detects the language of the instruct text and normalises it
+internally — you can write in English, Chinese, or a mix of both.
+
+## Supported Attributes
+
+### Gender
+
+| English | Chinese |
+|---------|---------|
+| male | 男 |
+| female | 女 |
+
+### Age
+
+| English | Chinese |
+|---------|---------|
+| child | 儿童 |
+| teenager | 少年 |
+| young adult | 青年 |
+| middle-aged | 中年 |
+| elderly | 老年 |
+
+### Pitch
+
+| English | Chinese |
+|---------|---------|
+| very low pitch | 极低音调 |
+| low pitch | 低音调 |
+| moderate pitch | 中音调 |
+| high pitch | 高音调 |
+| very high pitch | 极高音调 |
+
+### Style
+
+| English | Chinese |
+|---------|---------|
+| whisper | 耳语 |
+
+### English Accent
+
+Only effective when the synthesis text is in English.
+
+| Accent |
+|--------|
+| american accent |
+| british accent |
+| australian accent |
+| canadian accent |
+| indian accent |
+| chinese accent |
+| korean accent |
+| japanese accent |
+| portuguese accent |
+| russian accent |
+
+### Chinese Dialect
+
+Only effective when the synthesis text is in Chinese.
+
+| Dialect |
+|---------|
+| 河南话 |
+| 陕西话 |
+| 四川话 |
+| 贵州话 |
+| 云南话 |
+| 桂林话 |
+| 济南话 |
+| 石家庄话 |
+| 甘肃话 |
+| 宁夏话 |
+| 青岛话 |
+| 东北话 |
+
+## Writing Instruct Strings
+
+Separate attributes with commas (half-width `,` for English, full-width `,`
+for Chinese — the model auto-fixes mismatches).
+
+```
+# English
+"female, young adult, high pitch, british accent"
+
+# Chinese
+"女,青年,高音调,四川话"
+
+# Mixed (auto-normalised)
+"female, young adult, 四川话"
+```
+
+### Tips
+
+- **Combine freely** across categories: `"male, elderly, low pitch, whisper"`.
+- **Leave it to the model**: omit attributes you don't care about — the model
+ fills in the rest. For example `"female"` alone is valid.
+- **Case-insensitive**: `"Male"`, `"MALE"`, and `"male"` are all accepted, the code will normalize them to lower case.
+
+- **Accent vs Dialect**: English accents are only applied to English speech, Chinese dialects are only applied to Chinese speech.
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..ebdf6e86
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,118 @@
+# OmniVoice Examples
+
+This directory contains scripts and configs for training, fine-tuning, and evaluating OmniVoice.
+
+| Use Case | Script | Description |
+|---|---|---|
+| Training from scratch | [run_emilia.sh](run_emilia.sh) | Full pipeline on the Emilia dataset (data check, tokenization, training) |
+| Fine-tuning | [run_finetune.sh](run_finetune.sh) | Fine-tune from a pretrained checkpoint using your own JSONL data |
+| Evaluation | [run_eval.sh](run_eval.sh) | Evaluate WER, speaker similarity, and UTMOS on standard test sets |
+
+---
+
+## Training from Scratch (Emilia)
+
+[run_emilia.sh](run_emilia.sh) runs the full pipeline in 3 stages:
+
+| Stage | What it does |
+|---|---|
+| 0 | Verify the Emilia dataset and JSONL manifests are in place |
+| 1 | Tokenize audio into WebDataset shards |
+| 2 | Launch multi-GPU training with `accelerate` |
+
+**Prerequisites:**
+
+1. Download the Emilia dataset from [OpenXLab](https://openxlab.org.cn/datasets/Amphion/Emilia) and place it under `download/`:
+ ```
+ download/Amphion___Emilia
+ └── raw
+ ├── EN
+ └── ZH
+ ```
+2. Obtain JSONL manifests and place them in `data/emilia/manifests/`:
+ - `emilia_en_train.jsonl`, `emilia_en_dev.jsonl`
+ - `emilia_zh_train.jsonl`, `emilia_zh_dev.jsonl`
+
+ You can generate them from the raw data, or download pre-processed manifests from [HuggingFace](https://huggingface.co/datasets/zhu-han/Emilia-Manifests).
+
+**Run the full pipeline:**
+
+```bash
+bash examples/run_emilia.sh
+```
+
+Or run individual stages by setting `stage` and `stop_stage` at the top of the script (e.g. `stage=1`, `stop_stage=1` to only tokenize).
+
+> See [docs/training.md](../docs/training.md) for config details, checkpoint resuming, and TensorBoard monitoring.
+
+---
+
+## Fine-tuning
+
+[run_finetune.sh](run_finetune.sh) fine-tunes from a pretrained checkpoint on your own data.
+
+### Step 1: Prepare Your Data
+
+Create a JSONL manifest where each line describes one audio sample:
+
+```jsonl
+{"id": "sample_001", "audio_path": "/data/audio/001.wav", "text": "Hello world", "language_id": "en"}
+{"id": "sample_002", "audio_path": "/data/audio/002.wav", "text": "你好世界", "language_id": "zh"}
+```
+
+`id`, `audio_path`, and `text` are mandatory. `language_id` is optional.
+
+> See [docs/data_preparation.md](../docs/data_preparation.md) for the full data format specification.
+
+### Step 2: Configure the Script
+
+Edit the variables at the top of `run_finetune.sh`:
+
+```bash
+TRAIN_JSONL="data/my_data_train.jsonl" # path to training JSONL
+DEV_JSONL="data/my_data_dev.jsonl" # path to dev JSONL
+GPU_IDS="0,1" # GPUs to use
+NUM_GPUS=2
+OUTPUT_DIR="exp/omnivoice_finetune" # output directory
+```
+
+### Step 3: Run
+
+```bash
+bash examples/run_finetune.sh
+```
+
+The script will:
+1. Tokenize your audio into WebDataset shards
+2. Launch fine-tuning with `accelerate`
+
+Main difference between fine-tuning config ([config/train_config_finetune.json](config/train_config_finetune.json)) and the Emilia training config ([config/train_config_emilia.json](config/train_config_emilia.json)) are:
+
+| Parameter | Emilia (from scratch) | Fine-tune | Why |
+|---|---|---|---|
+| `init_from_checkpoint` | `null` | `"k2-fsa/OmniVoice"` | Load pretrained weights |
+| `steps` | 300,000 | 5,000 | Fewer steps for fine-tuning, can be tuned according to your data/task. |
+| `learning_rate` | 1e-4 | 5e-5 | Lower LR for fine-tuning, can be tuned according to your data/task |
+
+To use a different pretrained checkpoint, modify `init_from_checkpoint` in the config file.
+
+---
+
+## Evaluation
+
+Install evaluation dependencies first:
+
+```bash
+pip install omnivoice[eval]
+# or
+uv sync --extra eval
+```
+
+Supported test sets: `librispeech_pc`, `seedtts_en`, `seedtts_zh`, `fleurs`, `minimax`.
+
+```bash
+bash examples/run_eval.sh
+```
+
+> See [docs/evaluation.md](../docs/evaluation.md) for metrics details, test set preparation, and running individual metrics.
+
diff --git a/examples/config/data_config_emilia.json b/examples/config/data_config_emilia.json
new file mode 100644
index 00000000..665dd768
--- /dev/null
+++ b/examples/config/data_config_emilia.json
@@ -0,0 +1,36 @@
+{
+ "train":
+ [
+ {
+ "language_id": "en",
+ "manifest_path": [
+ "data/emilia/tokens/emilia_en_train/data.lst"
+ ],
+ "repeat": 1
+ },
+ {
+ "language_id": "zh",
+ "manifest_path": [
+ "data/emilia/tokens/emilia_zh_train/data.lst"
+ ],
+ "repeat": 1
+ }
+ ],
+ "dev":
+ [
+ {
+ "language_id": "en",
+ "manifest_path": [
+ "data/emilia/tokens/emilia_en_dev/data.lst"
+ ],
+ "repeat": 1
+ },
+ {
+ "language_id": "zh",
+ "manifest_path": [
+ "data/emilia/tokens/emilia_zh_dev/data.lst"
+ ],
+ "repeat": 1
+ }
+ ]
+}
diff --git a/examples/config/data_config_finetune.json b/examples/config/data_config_finetune.json
new file mode 100644
index 00000000..7b106dd9
--- /dev/null
+++ b/examples/config/data_config_finetune.json
@@ -0,0 +1,12 @@
+{
+ "train": [
+ {
+ "manifest_path": ["data/finetune/tokens/train/data.lst"]
+ }
+ ],
+ "dev": [
+ {
+ "manifest_path": ["data/finetune/tokens/dev/data.lst"]
+ }
+ ]
+}
diff --git a/examples/config/ds_config_zero2.json b/examples/config/ds_config_zero2.json
new file mode 100644
index 00000000..367893ee
--- /dev/null
+++ b/examples/config/ds_config_zero2.json
@@ -0,0 +1,19 @@
+{
+ "steps_per_print": 100,
+ "zero_optimization": {
+ "stage": 2,
+ "allgather_partitions": true,
+ "allgather_bucket_size": 2e8,
+ "overlap_comm": true,
+ "reduce_scatter": true,
+ "reduce_bucket_size": 2e8,
+ "contiguous_gradients": true
+ },
+ "gradient_accumulation_steps": "auto",
+ "gradient_clipping": "auto",
+ "train_batch_size": "auto",
+ "train_micro_batch_size_per_gpu": "auto",
+ "bf16": {
+ "enabled": "auto"
+ }
+}
diff --git a/examples/config/train_config_emilia.json b/examples/config/train_config_emilia.json
new file mode 100644
index 00000000..66dec81f
--- /dev/null
+++ b/examples/config/train_config_emilia.json
@@ -0,0 +1,39 @@
+{
+ "llm_name_or_path": "Qwen/Qwen3-0.6B",
+ "audio_vocab_size": 1025,
+ "audio_mask_id": 1024,
+ "num_audio_codebook": 8,
+
+ "audio_codebook_weights": [8, 8, 6, 6, 4, 4, 2, 2],
+ "drop_cond_ratio": 0.1,
+ "prompt_ratio_range": [0.0, 0.3],
+ "mask_ratio_range": [0.0, 1.0],
+ "language_ratio": 0.0,
+ "use_pinyin_ratio": 0.0,
+ "instruct_ratio": 0.0,
+ "only_instruct_ratio": 0.0,
+
+ "resume_from_checkpoint": null,
+ "init_from_checkpoint": null,
+
+ "learning_rate": 1e-4,
+ "weight_decay": 0.01,
+ "max_grad_norm": 1.0,
+ "steps": 300000,
+ "seed": 42,
+ "warmup_type": "ratio",
+ "warmup_ratio": 0.03,
+ "warmup_steps": 0,
+
+ "batch_tokens": 8192,
+ "gradient_accumulation_steps": 1,
+ "num_workers": 4,
+
+ "mixed_precision": "bf16",
+ "allow_tf32": true,
+
+ "logging_steps": 100,
+ "eval_steps": 1000,
+ "save_steps": 10000,
+ "keep_last_n_checkpoints": -1
+}
diff --git a/examples/config/train_config_finetune.json b/examples/config/train_config_finetune.json
new file mode 100644
index 00000000..23ce16a9
--- /dev/null
+++ b/examples/config/train_config_finetune.json
@@ -0,0 +1,39 @@
+{
+ "llm_name_or_path": "Qwen/Qwen3-0.6B",
+ "audio_vocab_size": 1025,
+ "audio_mask_id": 1024,
+ "num_audio_codebook": 8,
+
+ "audio_codebook_weights": [8, 8, 6, 6, 4, 4, 2, 2],
+ "drop_cond_ratio": 0.1,
+ "prompt_ratio_range": [0.0, 0.3],
+ "mask_ratio_range": [0.0, 1.0],
+ "language_ratio": 0.8,
+ "use_pinyin_ratio": 0.0,
+ "instruct_ratio": 0.0,
+ "only_instruct_ratio": 0.0,
+
+ "resume_from_checkpoint": null,
+ "init_from_checkpoint": "k2-fsa/OmniVoice",
+
+ "learning_rate": 5e-5,
+ "weight_decay": 0.01,
+ "max_grad_norm": 1.0,
+ "steps": 5000,
+ "seed": 42,
+ "warmup_type": "ratio",
+ "warmup_ratio": 0.01,
+ "warmup_steps": 0,
+
+ "batch_tokens": 8192,
+ "gradient_accumulation_steps": 1,
+ "num_workers": 2,
+
+ "mixed_precision": "bf16",
+ "allow_tf32": true,
+
+ "logging_steps": 50,
+ "eval_steps": 500,
+ "save_steps": 500,
+ "keep_last_n_checkpoints": -1
+}
diff --git a/examples/config/train_config_multilingual.json b/examples/config/train_config_multilingual.json
new file mode 100644
index 00000000..b1ee61cf
--- /dev/null
+++ b/examples/config/train_config_multilingual.json
@@ -0,0 +1,39 @@
+{
+ "llm_name_or_path": "Qwen/Qwen3-0.6B",
+ "audio_vocab_size": 1025,
+ "audio_mask_id": 1024,
+ "num_audio_codebook": 8,
+
+ "audio_codebook_weights": [8, 8, 6, 6, 4, 4, 2, 2],
+ "drop_cond_ratio": 0.1,
+ "prompt_ratio_range": [0.0, 0.3],
+ "mask_ratio_range": [0.0, 1.0],
+ "language_ratio": 0.8,
+ "use_pinyin_ratio": 0.3,
+ "instruct_ratio": 1.0,
+ "only_instruct_ratio": 0.5,
+
+ "resume_from_checkpoint": null,
+ "init_from_checkpoint": null,
+
+ "learning_rate": 1e-4,
+ "weight_decay": 0.01,
+ "max_grad_norm": 1.0,
+ "steps": 2000000,
+ "seed": 42,
+ "warmup_type": "ratio",
+ "warmup_ratio": 0.03,
+ "warmup_steps": 0,
+
+ "batch_tokens": 8192,
+ "gradient_accumulation_steps": 1,
+ "num_workers": 4,
+
+ "mixed_precision": "bf16",
+ "allow_tf32": true,
+
+ "logging_steps": 100,
+ "eval_steps": 1000,
+ "save_steps": 10000,
+ "keep_last_n_checkpoints": -1
+}
diff --git a/examples/run_emilia.sh b/examples/run_emilia.sh
new file mode 100755
index 00000000..40b869d5
--- /dev/null
+++ b/examples/run_emilia.sh
@@ -0,0 +1,115 @@
+#!/bin/bash
+
+# This script demonstrates how to run the full training pipeline on the Emilia dataset.
+
+set -euo pipefail
+
+stage=0
+stop_stage=2
+
+# ====== Modify as needed ======
+# GPUs to use
+GPU_IDS="0,1,2,3,4,5,6,7"
+NUM_GPUS=8
+
+# Download directory for raw Emilia data
+dl_dir="download"
+
+# Directory containing JSONL manifests for train/dev splits
+# Stage 0 will check for the presence of the following files:
+# data/emilia/manifests/emilia_en_train.jsonl
+# data/emilia/manifests/emilia_en_dev.jsonl
+# data/emilia/manifests/emilia_zh_train.jsonl
+# data/emilia/manifests/emilia_zh_dev.jsonl
+MANIFEST_DIR="data/emilia/manifests"
+
+# Directory to write tokenized WebDataset shards
+TOKEN_DIR="data/emilia/tokens"
+
+# Audio tokenizer model (HuggingFace repo or local path)
+TOKENIZER_PATH="eustlb/higgs-audio-v2-tokenizer"
+
+# Training config file
+TRAIN_CONFIG="config/train_config_emilia.json"
+
+# Data config file
+data_config="config/data_config_emilia.json"
+
+# Output directory for checkpoints
+OUTPUT_DIR="exp/omnivoice_emilia"
+# =================================
+
+export PYTHONPATH="$(cd "$(dirname "$0")/.." && pwd):${PYTHONPATH:-}"
+
+
+# Stage 0: Download data
+if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then
+ echo "Stage 0: Download data"
+
+ # You should manually download the Emilia dataset from
+ # https://openxlab.org.cn/datasets/Amphion/Emilia
+ # or https://huggingface.co/datasets/amphion/Emilia-Dataset/tree/fc71e07
+ # and place it in the download directory.
+ # Your download directory should at least contain the following structure:
+ #
+ # download/Amphion___Emilia
+ # ├── raw
+ # │ ├── EN
+ # │ └── ZH
+
+ if [ ! -d "$dl_dir"/Amphion___Emilia/raw ]; then
+ echo "Please refer https://openxlab.org.cn/datasets/Amphion/Emilia to download the dataset."
+ exit 1
+ fi
+
+ # We require JSONL manifests for the training and dev splits. You can
+ # either generate them yourself using the raw data and the provided
+ # metadata, or download our processed JSONL manifests from HuggingFace.
+ # https://huggingface.co/datasets/zhu-han/Emilia-Manifests
+ #
+ # Place them as data/emilia/manifests/{emilia_en_train,emilia_en_dev,emilia_zh_train,emilia_zh_dev}.jsonl
+
+ for split in emilia_en_dev emilia_zh_dev emilia_en_train emilia_zh_train; do
+ if [ ! -f "${MANIFEST_DIR}/${split}.jsonl" ]; then
+ echo "Please download the manifest for ${split} and place it in ${MANIFEST_DIR}/${split}.jsonl"
+ exit 1
+ fi
+ done
+
+ echo " Done. All manifests and data are in place."
+fi
+
+
+# Stage 1: Tokenize splits into directories matching data_config_emilia.json
+if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then
+ echo "Stage 1: Tokenizing audio"
+
+ for split in emilia_en_dev emilia_zh_dev emilia_en_train emilia_zh_train; do
+ echo " Tokenizing ${split} from ${MANIFEST_DIR}/${split}.jsonl"
+
+ CUDA_VISIBLE_DEVICES=${GPU_IDS} \
+ python -m omnivoice.scripts.extract_audio_tokens \
+ --input_jsonl "${MANIFEST_DIR}/${split}.jsonl" \
+ --tar_output_pattern "${TOKEN_DIR}/${split}/audios/shard-%06d.tar" \
+ --jsonl_output_pattern "${TOKEN_DIR}/${split}/txts/shard-%06d.jsonl" \
+ --tokenizer_path "${TOKENIZER_PATH}" \
+ --nj_per_gpu 3 \
+ --shuffle True
+
+ echo " Done. Tokens written to ${TOKEN_DIR}/${split}"
+ done
+fi
+
+
+# Stage 2: Train
+if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then
+ echo "Stage 2: Training"
+
+ accelerate launch \
+ --gpu_ids "${GPU_IDS}" \
+ --num_processes ${NUM_GPUS} \
+ -m omnivoice.cli.train \
+ --train_config ${TRAIN_CONFIG} \
+ --data_config ${data_config} \
+ --output_dir ${OUTPUT_DIR}
+fi
\ No newline at end of file
diff --git a/examples/run_eval.sh b/examples/run_eval.sh
new file mode 100755
index 00000000..23a0fdfc
--- /dev/null
+++ b/examples/run_eval.sh
@@ -0,0 +1,283 @@
+#!/bin/bash
+
+# Evaluate OmniVoice models on TTS benchmarks.
+
+# Stage 1: Download the test sets and evaluation models.
+# Stage 2: LibriSpeech-PC
+# Stage 3: seedtts_en
+# Stage 4: seedtts_zh
+# Stage 5: fleurs
+# Stage 6: minimax
+
+set -euo pipefail
+
+# Specify the stages to run by setting the `stage` and `stop_stage` variables.
+stage=1
+stop_stage=6
+
+# Available GPUs for evaluation. Adjust this according to your setup.
+export CUDA_VISIBLE_DEVICES="0,1,2,3,4,5,6,7"
+
+# Specify the checkpoint to evaluate.
+CHECKPOINT=k2-fsa/OmniVoice
+emilia_checkpoint=false
+
+# CHECKPOINT=k2-fsa/OmniVoice
+# emilia_checkpoint=true
+
+# For the OmniVoice-Emilia checkpoint, we set denoise to False and lang_id to None
+#, as the model is trained without prompt denoising or language id.
+
+if [ "${emilia_checkpoint}" = true ]; then
+ infer_options="--preprocess_prompt False \
+ --postprocess_output False \
+ --batch_duration 600 \
+ --denoise False \
+ --lang_id None \
+ --audio_chunk_threshold 1000"
+else
+ infer_options="--preprocess_prompt False \
+ --postprocess_output False \
+ --batch_duration 600 \
+ --audio_chunk_threshold 1000"
+fi
+
+export PYTHONPATH="$(cd "$(dirname "$0")/.." && pwd):${PYTHONPATH:-}"
+
+download_dir="download"
+TTS_EVAL_MODEL_DIR="${download_dir}/tts_eval_models/"
+TTS_EVAL_DATA_DIR="${download_dir}/tts_eval_datasets/"
+
+# Map test_name to its test.jsonl path.
+get_test_list() {
+ case "$1" in
+ librispeech_pc) echo "${TTS_EVAL_DATA_DIR}/librispeech_pc_test_clean.jsonl" ;;
+ seedtts_en) echo "${TTS_EVAL_DATA_DIR}/seedtts_test_en.jsonl" ;;
+ seedtts_zh) echo "${TTS_EVAL_DATA_DIR}/seedtts_test_zh.jsonl" ;;
+ minimax) echo "${TTS_EVAL_DATA_DIR}/minimax_multilingual_24.jsonl" ;;
+ fleurs) echo "${TTS_EVAL_DATA_DIR}/fleurs_multilingual_102.jsonl" ;;
+ *) echo ""; return 1 ;;
+ esac
+}
+
+# ============================================================
+# Stage 1: Prepare the test sets and evaluation models
+# ============================================================
+
+if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then
+ echo "Stage 1: Download test sets and evaluation models"
+
+ hf_repo=k2-fsa/TTS_eval_datasets
+ mkdir -p ${TTS_EVAL_DATA_DIR}/
+ for file in \
+ librispeech_pc_test_clean.jsonl \
+ librispeech_pc_test_clean_transcript.jsonl \
+ seedtts_test_en.jsonl \
+ seedtts_test_zh.jsonl \
+ minimax_multilingual_24.jsonl \
+ fleurs_multilingual_102.jsonl; do
+ echo "Downloading ${file}..."
+ huggingface-cli download \
+ --repo-type dataset \
+ --local-dir ${TTS_EVAL_DATA_DIR}/ \
+ ${hf_repo} \
+ ${file}
+ done
+
+ for file in \
+ librispeech_pc_testset.tar.gz \
+ seedtts_testset.tar.gz \
+ minimax_multilingual_24.tar.gz \
+ fleurs_multilingual_102.tar.gz; do
+ echo "Downloading ${file}..."
+ huggingface-cli download \
+ --repo-type dataset \
+ --local-dir ${TTS_EVAL_DATA_DIR}/ \
+ ${hf_repo} \
+ ${file}
+
+ echo "Extracting ${file}..."
+ tar -xzf ${TTS_EVAL_DATA_DIR}/${file} -C ${TTS_EVAL_DATA_DIR}/
+ done
+
+ echo "Download all evaluation models"
+ hf_repo=k2-fsa/TTS_eval_models
+ mkdir -p ${TTS_EVAL_MODEL_DIR}
+ huggingface-cli download \
+ --local-dir ${TTS_EVAL_MODEL_DIR} \
+ ${hf_repo}
+fi
+
+# ============================================================
+# Stage 2: Evaluation on LibriSpeech-PC
+# ============================================================
+
+
+if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then
+ echo "Stage 2: Evaluation on LibriSpeech-PC"
+ wav_path="results/librispeech_pc"
+ test_jsonl="$(get_test_list librispeech_pc)"
+ transcript_jsonl="${TTS_EVAL_DATA_DIR}/librispeech_pc_test_clean_transcript.jsonl"
+
+ python -m omnivoice.cli.infer_batch \
+ --model "${CHECKPOINT}" \
+ --test_list "${test_jsonl}" \
+ --res_dir "${wav_path}" ${infer_options}
+
+ python -m omnivoice.eval.speaker_similarity.sim \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.sim.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ python -m omnivoice.eval.wer.hubert \
+ --wav-path "${wav_path}" \
+ --test-list "${transcript_jsonl}" \
+ --decode-path "${wav_path}.wer.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ python -m omnivoice.eval.mos.utmos \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.mos.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+fi
+
+
+# ============================================================
+# Stage 3: Evaluation on Seed-TTS en
+# ============================================================
+
+if [ ${stage} -le 3 ] && [ ${stop_stage} -ge 3 ]; then
+ echo "Stage 3: Evaluation on Seed-TTS en"
+ wav_path="results/seedtts_en"
+ test_jsonl="$(get_test_list seedtts_en)"
+
+ python -m omnivoice.cli.infer_batch \
+ --model "${CHECKPOINT}" \
+ --test_list "${test_jsonl}" \
+ --res_dir "${wav_path}" ${infer_options}
+
+
+ python -m omnivoice.eval.speaker_similarity.sim \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.sim.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ python -m omnivoice.eval.wer.seedtts \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.wer.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}" \
+ --lang en
+
+ python -m omnivoice.eval.mos.utmos \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.mos.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+fi
+
+
+# ============================================================
+# Stage 4: Evaluation on Seed-TTS zh
+# ============================================================
+
+if [ ${stage} -le 4 ] && [ ${stop_stage} -ge 4 ]; then
+ echo "Stage 4: Evaluation on Seed-TTS zh"
+ wav_path="results/seedtts_zh"
+ test_jsonl="$(get_test_list seedtts_zh)"
+
+ python -m omnivoice.cli.infer_batch \
+ --model "${CHECKPOINT}" \
+ --test_list "${test_jsonl}" \
+ --res_dir "${wav_path}" ${infer_options}
+
+
+ python -m omnivoice.eval.speaker_similarity.sim \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.sim.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ python -m omnivoice.eval.wer.seedtts \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.wer.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}" \
+ --lang zh
+
+ python -m omnivoice.eval.mos.utmos \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.mos.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+fi
+
+
+
+# ============================================================
+# Stage 5: Evaluation on MiniMax multilingual
+# ============================================================
+
+if [ ${stage} -le 5 ] && [ ${stop_stage} -ge 5 ]; then
+ echo "Stage 5: Evaluation on MiniMax multilingual"
+ wav_path="results/minimax"
+ test_jsonl="$(get_test_list minimax)"
+
+ python -m omnivoice.cli.infer_batch \
+ --model "${CHECKPOINT}" \
+ --test_list "${test_jsonl}" \
+ --res_dir "${wav_path}" ${infer_options}
+
+ python -m omnivoice.eval.speaker_similarity.sim \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.sim.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ python -m omnivoice.eval.wer.minimax \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.wer.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+fi
+
+
+# ============================================================
+# Stage 6: Evaluation on FLEURS multilingual
+# ============================================================
+
+if [ ${stage} -le 6 ] && [ ${stop_stage} -ge 6 ]; then
+ echo "Stage 6: Evaluation on FLEURS multilingual"
+ wav_path="results/fleurs"
+ test_jsonl="$(get_test_list fleurs)"
+
+ python -m omnivoice.cli.infer_batch \
+ --model "${CHECKPOINT}" \
+ --test_list "${test_jsonl}" \
+ --res_dir "${wav_path}" ${infer_options}
+
+
+ python -m omnivoice.eval.speaker_similarity.sim \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.sim.log" \
+ --model-dir "${TTS_EVAL_MODEL_DIR}"
+
+ # Evaluation on FLEURS requires omnilingual-asr, which has dependencies that
+ # conflict with other packages (at least the transformers package) in our project.
+
+ # To evaluate on FLEURS, we suggest users to set up a separate virtual
+ # environment to install omnilingual-asr. Install instructions can be found in
+ # https://github.com/facebookresearch/omnilingual-asr
+
+ python ${PWD}/../omnivoice/eval/wer/fleurs.py \
+ --wav-path "${wav_path}" \
+ --test-list "${test_jsonl}" \
+ --decode-path "${wav_path}.wer.log" \
+ --model-card omniASR_LLM_Unlimited_7B_v2 \
+ --chunk-size 100 \
+ --batch-size 50
+fi
diff --git a/examples/run_finetune.sh b/examples/run_finetune.sh
new file mode 100755
index 00000000..db8ee2a0
--- /dev/null
+++ b/examples/run_finetune.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+
+# This script demonstrates how to fine-tune OmniVoice from a JSONL manifest.
+
+set -euo pipefail
+
+stage=0
+stop_stage=1
+
+# ====== Modify as needed ======
+# GPUs to use
+GPU_IDS="0,1"
+NUM_GPUS=2
+
+# Path to your input JSONL file
+# (each line: {"id": ..., "audio_path": ..., "text": ..., "language_id": ...})
+TRAIN_JSONL="data/my_data_train.jsonl"
+
+# Path to your dev JSONL file. Set to empty string to skip dev set.
+DEV_JSONL="data/my_data_dev.jsonl"
+
+# Directory to write tokenized WebDataset shards
+TOKEN_DIR="data/finetune/tokens"
+
+# Audio tokenizer model (HuggingFace repo or local path)
+TOKENIZER_PATH="eustlb/higgs-audio-v2-tokenizer"
+
+# Training config file
+TRAIN_CONFIG="config/train_config_finetune.json"
+
+# Data config file
+data_config="config/data_config_finetune.json"
+
+# Output directory for fine-tuned checkpoints
+OUTPUT_DIR="exp/omnivoice_finetune"
+# =================================
+
+export PYTHONPATH="$(cd "$(dirname "$0")/.." && pwd):${PYTHONPATH:-}"
+
+
+# Stage 0: Tokenize audio into WebDataset shards
+if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then
+ echo "Stage 0: Tokenizing audio"
+
+ for split_jsonl_path in ${TRAIN_JSONL} ${DEV_JSONL}; do
+ if [ -z "${split_jsonl_path}" ]; then
+ continue
+ fi
+
+ if [ "${split_jsonl_path}" = "${TRAIN_JSONL}" ]; then
+ split="train"
+ else
+ split="dev"
+ fi
+
+ echo " Tokenizing ${split} from ${split_jsonl_path}"
+
+ CUDA_VISIBLE_DEVICES=${GPU_IDS} \
+ python -m omnivoice.scripts.extract_audio_tokens \
+ --input_jsonl "${split_jsonl_path}" \
+ --tar_output_pattern "${TOKEN_DIR}/${split}/audios/shard-%06d.tar" \
+ --jsonl_output_pattern "${TOKEN_DIR}/${split}/txts/shard-%06d.jsonl" \
+ --tokenizer_path "${TOKENIZER_PATH}" \
+ --nj_per_gpu 3 \
+ --shuffle True
+
+ echo " Done. Manifest written to ${TOKEN_DIR}/${split}/data.lst"
+ done
+fi
+
+
+# Stage 1: Fine-tune
+if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then
+ echo "Stage 1: Fine-tuning"
+
+ accelerate launch \
+ --gpu_ids "${GPU_IDS}" \
+ --num_processes ${NUM_GPUS} \
+ -m omnivoice.cli.train \
+ --train_config ${TRAIN_CONFIG} \
+ --data_config ${data_config} \
+ --output_dir ${OUTPUT_DIR}
+fi
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..a36934d8
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..4fa125da
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..26545394
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ OmniVoice Studio
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 00000000..346a3629
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,2613 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "lucide-react": "^1.8.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "vite": "^8.0.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
+ "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.124.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
+ "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
+ "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
+ "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
+ "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
+ "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.9.2",
+ "@emnapi/runtime": "1.9.2",
+ "@napi-rs/wasm-runtime": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
+ "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
+ "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.7",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
+ "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.334",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
+ "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
+ "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.124.0",
+ "@rolldown/pluginutils": "1.0.0-rc.15"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.15",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
+ "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
+ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.15",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..b7b26030
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "omnivoice-studio",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "lucide-react": "^1.8.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "vite": "^8.0.4"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 00000000..f90339d8
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,184 @@
+.counter {
+ font-size: 16px;
+ padding: 5px 10px;
+ border-radius: 5px;
+ color: var(--accent);
+ background: var(--accent-bg);
+ border: 2px solid transparent;
+ transition: border-color 0.3s;
+ margin-bottom: 24px;
+
+ &:hover {
+ border-color: var(--accent-border);
+ }
+ &:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+.hero {
+ position: relative;
+
+ .base,
+ .framework,
+ .vite {
+ inset-inline: 0;
+ margin: 0 auto;
+ }
+
+ .base {
+ width: 170px;
+ position: relative;
+ z-index: 0;
+ }
+
+ .framework,
+ .vite {
+ position: absolute;
+ }
+
+ .framework {
+ z-index: 1;
+ top: 34px;
+ height: 28px;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
+ scale(1.4);
+ }
+
+ .vite {
+ z-index: 0;
+ top: 107px;
+ height: 26px;
+ width: auto;
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
+ scale(0.8);
+ }
+}
+
+#center {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+ place-content: center;
+ place-items: center;
+ flex-grow: 1;
+
+ @media (max-width: 1024px) {
+ padding: 32px 20px 24px;
+ gap: 18px;
+ }
+}
+
+#next-steps {
+ display: flex;
+ border-top: 1px solid var(--border);
+ text-align: left;
+
+ & > div {
+ flex: 1 1 0;
+ padding: 32px;
+ @media (max-width: 1024px) {
+ padding: 24px 20px;
+ }
+ }
+
+ .icon {
+ margin-bottom: 16px;
+ width: 22px;
+ height: 22px;
+ }
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+
+#docs {
+ border-right: 1px solid var(--border);
+
+ @media (max-width: 1024px) {
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ }
+}
+
+#next-steps ul {
+ list-style: none;
+ padding: 0;
+ display: flex;
+ gap: 8px;
+ margin: 32px 0 0;
+
+ .logo {
+ height: 18px;
+ }
+
+ a {
+ color: var(--text-h);
+ font-size: 16px;
+ border-radius: 6px;
+ background: var(--social-bg);
+ display: flex;
+ padding: 6px 12px;
+ align-items: center;
+ gap: 8px;
+ text-decoration: none;
+ transition: box-shadow 0.3s;
+
+ &:hover {
+ box-shadow: var(--shadow);
+ }
+ .button-icon {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ @media (max-width: 1024px) {
+ margin-top: 20px;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ li {
+ flex: 1 1 calc(50% - 8px);
+ }
+
+ a {
+ width: 100%;
+ justify-content: center;
+ box-sizing: border-box;
+ }
+ }
+}
+
+#spacer {
+ height: 88px;
+ border-top: 1px solid var(--border);
+ @media (max-width: 1024px) {
+ height: 48px;
+ }
+}
+
+.ticks {
+ position: relative;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ top: -4.5px;
+ border: 5px solid transparent;
+ }
+
+ &::before {
+ left: 0;
+ border-left-color: var(--border);
+ }
+ &::after {
+ right: 0;
+ border-right-color: var(--border);
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 00000000..1efacea6
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,790 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+import './index.css';
+import ALL_LANGUAGES from './languages.json';
+import {
+ Sparkles, Fingerprint, Wand2, SlidersHorizontal, UserSquare2, ShieldCheck,
+ Download as DownloadIcon, History, Command, Globe, Volume2, UploadCloud,
+ Settings2, ChevronDown, ChevronUp, Play, Search, Film, Trash2,
+ FileText, Loader, Check, AlertCircle, Plus, User, Save, Languages, Headphones
+} from 'lucide-react';
+
+const TAGS = [
+ '[laughter]', '[sigh]', '[confirmation-en]', '[question-en]',
+ '[question-ah]', '[question-oh]', '[question-ei]', '[question-yi]',
+ '[surprise-ah]', '[surprise-oh]', '[surprise-wa]', '[surprise-yo]',
+ '[dissatisfaction-hnn]'
+];
+
+const CATEGORIES = {
+ Gender: ["Auto", "male", "female"],
+ Age: ["Auto", "child", "teenager", "young adult", "middle-aged", "elderly"],
+ Pitch: ["Auto", "very low pitch", "low pitch", "moderate pitch", "high pitch", "very high pitch"],
+ Style: ["Auto", "whisper"],
+ EnglishAccent: ["Auto", "american accent", "british accent", "australian accent", "canadian accent", "indian accent", "chinese accent", "korean accent", "japanese accent", "portuguese accent", "russian accent"],
+ ChineseDialect: ["Auto", "河南话", "陕西话", "四川话", "贵州话", "云南话", "桂林话", "济南话", "石家庄话", "甘肃话", "宁夏话", "青岛话", "东北话"]
+};
+
+const PRESETS = [
+ { id: 'narrator', name: '🎙️ Authoritative', tags: '', attrs: {Gender:'male', Age:'middle-aged', Pitch:'low pitch', Style:'Auto', EnglishAccent:'british accent', ChineseDialect:'Auto'} },
+ { id: 'excited_child', name: '🧒 Excited Child', tags: '[laughter] ', attrs: {Gender:'Auto', Age:'child', Pitch:'high pitch', Style:'Auto', EnglishAccent:'Auto', ChineseDialect:'Auto'} },
+ { id: 'anxious_whisper', name: '🤫 Whisper', tags: '[question-en] ', attrs: {Gender:'Auto', Age:'young adult', Pitch:'Auto', Style:'whisper', EnglishAccent:'Auto', ChineseDialect:'Auto'} },
+ { id: 'surprised_woman', name: '😲 Surprised', tags: '[surprise-wa] ', attrs: {Gender:'female', Age:'young adult', Pitch:'high pitch', Style:'Auto', EnglishAccent:'Auto', ChineseDialect:'Auto'} },
+ { id: 'elderly_story', name: '👴 Elder', tags: '[sigh] ', attrs: {Gender:'male', Age:'elderly', Pitch:'very low pitch', Style:'Auto', EnglishAccent:'Auto', ChineseDialect:'Auto'} },
+ { id: 'sichuan', name: '🌶️ 四川话', tags: '', attrs: {Gender:'female', Age:'young adult', Pitch:'moderate pitch', Style:'Auto', EnglishAccent:'Auto', ChineseDialect:'四川话'} },
+];
+
+// Common ISO 639-1 codes for YouTube dubbing
+const LANG_CODES = [
+ {code:'en', label:'English'}, {code:'es', label:'Spanish'}, {code:'fr', label:'French'},
+ {code:'de', label:'German'}, {code:'it', label:'Italian'}, {code:'pt', label:'Portuguese'},
+ {code:'ru', label:'Russian'}, {code:'ja', label:'Japanese'}, {code:'ko', label:'Korean'},
+ {code:'zh', label:'Chinese'}, {code:'ar', label:'Arabic'}, {code:'hi', label:'Hindi'},
+ {code:'tr', label:'Turkish'}, {code:'pl', label:'Polish'}, {code:'nl', label:'Dutch'},
+ {code:'sv', label:'Swedish'}, {code:'th', label:'Thai'}, {code:'vi', label:'Vietnamese'},
+ {code:'id', label:'Indonesian'}, {code:'uk', label:'Ukrainian'},
+];
+
+const API = "http://localhost:8000";
+
+function formatTime(s) {
+ const m = Math.floor(s / 60);
+ const sec = (s % 60).toFixed(1);
+ return `${m}:${sec.padStart(4, '0')}`;
+}
+
+function App() {
+ const [mode, setMode] = useState('design');
+ const [text, setText] = useState('');
+ const [refAudio, setRefAudio] = useState(null);
+ const [refText, setRefText] = useState('');
+ const [instruct, setInstruct] = useState('');
+ const [language, setLanguage] = useState('Auto');
+ const [langSearch, setLangSearch] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [history, setHistory] = useState([]);
+
+ const [speed, setSpeed] = useState(1.0);
+ const [steps, setSteps] = useState(16);
+ const [cfg, setCfg] = useState(2.0);
+ const [showOverrides, setShowOverrides] = useState(false);
+ const [denoise, setDenoise] = useState(true);
+ const [tShift, setTShift] = useState(0.1);
+ const [posTemp, setPosTemp] = useState(5.0);
+ const [classTemp, setClassTemp] = useState(0.0);
+ const [layerPenalty, setLayerPenalty] = useState(5.0);
+ const [postprocess, setPostprocess] = useState(true);
+ const [duration, setDuration] = useState('');
+
+ const [vdStates, setVdStates] = useState({
+ Gender: 'Auto', Age: 'Auto', Pitch: 'Auto', Style: 'Auto', EnglishAccent: 'Auto', ChineseDialect: 'Auto'
+ });
+
+ const [generationTime, setGenerationTime] = useState(0);
+ const timerRef = useRef(null);
+ const textAreaRef = useRef(null);
+
+ // ═══ VOICE PROFILES ═══
+ const [profiles, setProfiles] = useState([]);
+ const [selectedProfile, setSelectedProfile] = useState(null);
+ const [showSaveProfile, setShowSaveProfile] = useState(false);
+ const [profileName, setProfileName] = useState('');
+
+ // ═══ DUB STATE ═══
+ const [dubJobId, setDubJobId] = useState(null);
+ const [dubStep, setDubStep] = useState('idle');
+ const [dubSegments, setDubSegments] = useState([]);
+ const [dubLang, setDubLang] = useState('Auto');
+ const [dubLangSearch, setDubLangSearch] = useState('');
+ const [dubLangCode, setDubLangCode] = useState('en');
+ const [dubInstruct, setDubInstruct] = useState('');
+ const [dubProgress, setDubProgress] = useState({ current: 0, total: 0, text: '' });
+ const [dubFilename, setDubFilename] = useState('');
+ const [dubDuration, setDubDuration] = useState(0);
+ const [dubError, setDubError] = useState('');
+ const [dubVideoFile, setDubVideoFile] = useState(null);
+ const [dubTracks, setDubTracks] = useState([]);
+ const [dubTranscript, setDubTranscript] = useState('');
+ const [showTranscript, setShowTranscript] = useState(false);
+ const [isTranslating, setIsTranslating] = useState(false);
+ const [previewAudios, setPreviewAudios] = useState({});
+ const [dubHistory, setDubHistory] = useState([]);
+ const [preserveBg, setPreserveBg] = useState(true);
+ const [makeDefaultTrack, setMakeDefaultTrack] = useState(true);
+
+ // ── LOAD DATA FROM SERVER ──
+ const loadProfiles = useCallback(async () => {
+ try {
+ const res = await fetch(`${API}/profiles`);
+ if (res.ok) setProfiles(await res.json());
+ } catch (e) {}
+ }, []);
+
+ const loadHistory = useCallback(async () => {
+ try {
+ const res = await fetch(`${API}/history`);
+ if (res.ok) setHistory(await res.json());
+ } catch (e) {}
+ }, []);
+
+ const loadDubHistory = useCallback(async () => {
+ try {
+ const res = await fetch(`${API}/dub/history`);
+ if (res.ok) setDubHistory(await res.json());
+ } catch (e) {}
+ }, []);
+
+ useEffect(() => {
+ loadProfiles();
+ loadHistory();
+ loadDubHistory();
+ // Restore local UI state
+ try {
+ const saved = JSON.parse(localStorage.getItem('omni_ui') || '{}');
+ if (saved.text) setText(saved.text);
+ if (saved.mode) setMode(saved.mode);
+ if (saved.vdStates) setVdStates(saved.vdStates);
+ if (saved.language) setLanguage(saved.language);
+ // Dub state
+ if (saved.dubJobId) setDubJobId(saved.dubJobId);
+ if (saved.dubFilename) setDubFilename(saved.dubFilename);
+ if (saved.dubDuration !== undefined) setDubDuration(saved.dubDuration);
+ if (saved.dubSegments) setDubSegments(saved.dubSegments);
+ if (saved.dubLang) setDubLang(saved.dubLang);
+ if (saved.dubLangCode) setDubLangCode(saved.dubLangCode);
+ if (saved.dubTracks) setDubTracks(saved.dubTracks);
+ if (saved.dubStep) setDubStep(saved.dubStep);
+ if (saved.dubTranscript) setDubTranscript(saved.dubTranscript);
+ } catch (e) {}
+ }, []);
+
+ useEffect(() => {
+ localStorage.setItem('omni_ui', JSON.stringify({
+ text, mode, vdStates, language,
+ dubJobId, dubFilename, dubDuration, dubSegments,
+ dubLang, dubLangCode, dubTracks, dubStep, dubTranscript
+ }));
+ }, [text, mode, vdStates, language, dubJobId, dubFilename, dubDuration, dubSegments, dubLang, dubLangCode, dubTracks, dubStep, dubTranscript]);
+
+ // ── TTS ──
+ const insertTag = (tag) => {
+ if (!textAreaRef.current) return;
+ const start = textAreaRef.current.selectionStart;
+ const end = textAreaRef.current.selectionEnd;
+ setText(text.substring(0, start) + tag + text.substring(end));
+ setTimeout(() => { textAreaRef.current.focus(); textAreaRef.current.setSelectionRange(start + tag.length, start + tag.length); }, 0);
+ };
+
+ const applyPreset = (preset) => {
+ setVdStates(preset.attrs);
+ if (preset.tags && !text.includes(preset.tags.trim())) insertTag(preset.tags);
+ };
+
+ const handleGenerate = async () => {
+ if (!text.trim()) return alert("Please enter text");
+ if (mode === 'clone' && !refAudio && !selectedProfile) return alert("Upload an audio or select a voice profile");
+ setIsGenerating(true);
+ setGenerationTime(0);
+ const st = Date.now();
+ timerRef.current = setInterval(() => setGenerationTime(((Date.now() - st) / 1000).toFixed(1)), 100);
+ try {
+ const formData = new FormData();
+ formData.append("text", text);
+ if (language !== 'Auto') formData.append("language", language);
+ formData.append("num_step", steps);
+ formData.append("guidance_scale", cfg);
+ formData.append("speed", speed);
+ formData.append("denoise", denoise);
+ formData.append("t_shift", tShift);
+ formData.append("position_temperature", posTemp);
+ formData.append("class_temperature", classTemp);
+ formData.append("layer_penalty_factor", layerPenalty);
+ formData.append("postprocess_output", postprocess);
+ if (duration) formData.append("duration", parseFloat(duration));
+
+ if (mode === 'clone') {
+ if (selectedProfile) {
+ formData.append("profile_id", selectedProfile);
+ } else if (refAudio) {
+ formData.append("ref_audio", refAudio);
+ formData.append("ref_text", refText);
+ }
+ if (instruct) formData.append("instruct", instruct);
+ } else {
+ const parts = Object.values(vdStates).filter(v => v !== 'Auto');
+ if (instruct.trim()) parts.push(instruct.trim());
+ const finalInstruct = parts.join(', ');
+ if (finalInstruct) formData.append("instruct", finalInstruct);
+ }
+
+ const response = await fetch(`${API}/generate`, { method: "POST", body: formData });
+ if (!response.ok) throw new Error(await response.text());
+ // Refresh history from server
+ await loadHistory();
+ } catch (err) {
+ alert("Error: " + err.message);
+ } finally {
+ clearInterval(timerRef.current);
+ setIsGenerating(false);
+ }
+ };
+
+ // ── PROFILES ──
+ const handleSaveProfile = async () => {
+ if (!profileName.trim() || !refAudio) return alert("Need a name and reference audio");
+ const formData = new FormData();
+ formData.append("name", profileName);
+ formData.append("ref_audio", refAudio);
+ formData.append("ref_text", refText);
+ formData.append("instruct", instruct);
+ formData.append("language", language);
+ try {
+ const res = await fetch(`${API}/profiles`, { method: "POST", body: formData });
+ if (!res.ok) throw new Error(await res.text());
+ setShowSaveProfile(false);
+ setProfileName('');
+ await loadProfiles();
+ } catch (e) { alert(e.message); }
+ };
+
+ const handleDeleteProfile = async (id) => {
+ if (!confirm("Delete this voice profile?")) return;
+ await fetch(`${API}/profiles/${id}`, { method: "DELETE" });
+ if (selectedProfile === id) setSelectedProfile(null);
+ await loadProfiles();
+ };
+
+ const handleSelectProfile = (profile) => {
+ setSelectedProfile(profile.id);
+ setRefText(profile.ref_text || '');
+ setInstruct(profile.instruct || '');
+ if (profile.language && profile.language !== 'Auto') setLanguage(profile.language);
+ };
+
+ // ═══ DUB WORKFLOW ═══
+ const handleDubUpload = async () => {
+ if (!dubVideoFile) return;
+ setDubStep('uploading'); setDubError(''); setDubTracks([]);
+ try {
+ const fd = new FormData();
+ fd.append("video", dubVideoFile);
+ const res = await fetch(`${API}/dub/upload`, { method: "POST", body: fd });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ setDubJobId(data.job_id); setDubFilename(data.filename); setDubDuration(data.duration);
+ setDubStep('transcribing');
+ const tRes = await fetch(`${API}/dub/transcribe/${data.job_id}`, { method: "POST" });
+ if (!tRes.ok) throw new Error(await tRes.text());
+ const tData = await tRes.json();
+ setDubSegments(tData.segments.map((s, i) => ({ ...s, id: i })));
+ setDubTranscript(tData.full_transcript || '');
+ setDubStep('editing');
+ } catch (err) { setDubError(err.message); setDubStep('idle'); }
+ };
+
+ // ── AUTO-TRANSLATE ──
+ const handleTranslateAll = async () => {
+ if (!dubSegments.length || !dubLangCode) return;
+ setIsTranslating(true);
+ try {
+ const res = await fetch(`${API}/dub/translate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ segments: dubSegments.map(s => ({ id: s.id, text: s.text })),
+ target_lang: dubLangCode,
+ }),
+ });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ const translatedMap = {};
+ data.translated.forEach(t => { translatedMap[t.id] = t.text; });
+ setDubSegments(dubSegments.map(s => ({ ...s, text: translatedMap[s.id] || s.text })));
+ } catch (err) { setDubError('Translation failed: ' + err.message); }
+ setIsTranslating(false);
+ };
+
+ const handleDubGenerate = async () => {
+ setDubStep('generating');
+ setDubProgress({ current: 0, total: dubSegments.length, text: '' });
+ setDubError('');
+ try {
+ const body = {
+ segments: dubSegments.map(s => ({
+ start: s.start, end: s.end, text: s.text,
+ instruct: s.instruct || '', profile_id: s.profile_id || '',
+ })),
+ language: dubLang === 'Auto' ? 'Auto' : dubLang,
+ language_code: dubLangCode,
+ instruct: dubInstruct,
+ num_step: steps, guidance_scale: cfg, speed: speed,
+ };
+ const res = await fetch(`${API}/dub/generate/${dubJobId}`, {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n'); buffer = lines.pop();
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ try {
+ const evt = JSON.parse(line.slice(6));
+ if (evt.type === 'progress') setDubProgress({ current: evt.current + 1, total: evt.total, text: evt.text });
+ else if (evt.type === 'done') { setDubStep('done'); setDubTracks(evt.tracks || []); }
+ else if (evt.type === 'error') setDubError(p => p + `\nSeg ${evt.segment}: ${evt.error}`);
+ } catch (e) {}
+ }
+ }
+ }
+ if (dubStep !== 'done') setDubStep('done');
+ loadDubHistory();
+ } catch (err) { setDubError(err.message); setDubStep('editing'); }
+ };
+
+ const triggerDownload = (url, fallbackName) => {
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fallbackName || 'download';
+ a.target = '_blank';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ };
+ const handleDubDownload = () => triggerDownload(`${API}/dub/download/${dubJobId}/dubbed_video.mp4?preserve_bg=${preserveBg}&make_default=${makeDefaultTrack}`, 'dubbed_video.mp4');
+ const handleDubAudioDownload = () => triggerDownload(`${API}/dub/download-audio/${dubJobId}/dubbed_audio.wav?preserve_bg=${preserveBg}`, 'dubbed_audio.wav');
+ const resetDub = () => {
+ setDubJobId(null); setDubStep('idle'); setDubSegments([]); setDubFilename('');
+ setDubDuration(0); setDubError(''); setDubVideoFile(null); setDubTracks([]);
+ setDubProgress({ current: 0, total: 0, text: '' }); setDubTranscript(''); setShowTranscript(false);
+ setPreviewAudios({});
+ };
+
+ const restoreDubHistory = (item) => {
+ try {
+ if (!item.job_data) return;
+ const job = JSON.parse(item.job_data);
+ setMode('dub');
+ setDubJobId(item.id);
+ setDubFilename(job.filename || '');
+ setDubDuration(job.duration || 0);
+ setDubSegments((job.segments || []).map((s, i) => ({ ...s, id: s.id !== undefined ? s.id : i })));
+ setDubTranscript(job.full_transcript || '');
+ setDubLang(item.language || 'Auto');
+ setDubLangCode(item.language_code || 'und');
+ setDubTracks(Object.keys(job.dubbed_tracks || {}));
+ setDubStep(Object.keys(job.dubbed_tracks || {}).length > 0 ? 'done' : 'editing');
+ } catch (e) {
+ console.error("Failed to restore job_data", e);
+ }
+ };
+
+ const filteredLangs = langSearch ? ALL_LANGUAGES.filter(l => l.toLowerCase().includes(langSearch.toLowerCase())) : ALL_LANGUAGES;
+ const filteredDubLangs = dubLangSearch ? ALL_LANGUAGES.filter(l => l.toLowerCase().includes(dubLangSearch.toLowerCase())) : ALL_LANGUAGES;
+
+ return (
+
+
+
+
+
+
OmniVoice Studio
+
646 languages · Clone · Design · Video Dubbing
+
+
+
+
+ setMode('clone')}> Clone
+ setMode('design')}> Design
+ setMode('dub')}> Dub
+
+
+ {/* ═══ DUB TAB ═══ */}
+ {mode === 'dub' ? (
+
+ {dubStep === 'idle' && (
+
+
Upload Video for Dubbing
+
+
+ {dubVideoFile ? {dubVideoFile.name} ({(dubVideoFile.size/1024/1024).toFixed(1)} MB) : "MP4 / MOV / MKV / WEBM"}
+
+
setDubVideoFile(e.target.files[0])} style={{display:'none'}} id="video-upload"/>
+
Upload & Extract Audio
+
+ )}
+
+ {(dubStep === 'uploading' || dubStep === 'transcribing') && (
+
+
+
{dubStep === 'uploading' ? 'Extracting audio...' : 'Transcribing with Whisper...'}
+
+ )}
+
+ {(dubStep === 'editing' || dubStep === 'generating' || dubStep === 'done') && (
+
+
+
+
+ {dubFilename} ({formatTime(dubDuration)}) — {dubSegments.length} segments
+
+
Reset
+
+
+ {/* Full Transcript */}
+ {dubTranscript && (
+
+
setShowTranscript(!showTranscript)} style={{marginTop:0}}>
+ Full Transcript
+ {showTranscript ? : }
+
+ {showTranscript && (
+
+ {dubTranscript}
+
+ )}
+
+ )}
+
+
+
+
Target Language
+
setDubLang(e.target.value)}>
+ {filteredDubLangs.map(l => {l} )}
+
+
+
+
ISO Code (YouTube)
+
setDubLangCode(e.target.value)}>
+ {LANG_CODES.map(lc => {lc.code} — {lc.label} )}
+
+
+
+
Voice Style
+
setDubInstruct(e.target.value)}/>
+
+
+
+ {/* Translate All button */}
+
+
+ {isTranslating ? : }
+ {isTranslating ? 'Translating...' : `Translate All → ${LANG_CODES.find(l=>l.code===dubLangCode)?.label || dubLangCode}`}
+
+
+
+
+
+ Time
+ Text
+ Voice
+
+
+ {dubSegments.map((seg, idx) => (
+
idx + 1 ? 'segment-done' : ''}`}>
+
{formatTime(seg.start)}–{formatTime(seg.end)}
+
setDubSegments(dubSegments.map(s => s.id===seg.id ? {...s, text:e.target.value} : s))}
+ disabled={dubStep === 'generating'}/>
+
setDubSegments(dubSegments.map(s => s.id===seg.id ? {...s, profile_id:e.target.value} : s))}
+ disabled={dubStep === 'generating'}>
+ Default
+ {profiles.map(p => {p.name} )}
+
+
+ {dubStep === 'done' && (
+ {
+ const audio = new Audio(`${API}/dub/preview/${dubJobId}/${idx}`);
+ audio.play();
+ setPreviewAudios({...previewAudios, [idx]: audio});
+ }}>
+ )}
+ setDubSegments(dubSegments.filter(s => s.id !== seg.id))} disabled={dubStep === 'generating'}>
+
+
+ ))}
+
+
+
+
+ {dubStep === 'generating' && (
+
+
+ Dubbing {dubProgress.current}/{dubProgress.total}
+ {dubProgress.text}
+
+
+
+ )}
+ {dubStep === 'done' && (
+
+
+ Done! Tracks: {dubTracks.join(', ')}
+
+ )}
+ {dubError && (
+
+ {dubError}
+
+ )}
+
+
+ {dubStep==='generating' ? : }
+ {dubStep==='generating' ? 'Dubbing...' : 'Generate Dub Track'}
+
+
+ YouTube-Ready MP4
+
+
+ Audio Only (WAV)
+
+
triggerDownload(`${API}/dub/srt/${dubJobId}/subtitles.srt`, 'subtitles.srt')}
+ disabled={!dubSegments.length}
+ style={{marginTop:0, background: dubSegments.length ? 'linear-gradient(135deg,#d3869b,#b16286)' : undefined}}>
+ SRT Subtitles
+
+
+ {dubStep === 'done' && (
+
+ )}
+
+
+ )}
+
+ ) : (
+ <>
+ {/* ═══ CLONE / DESIGN ═══ */}
+
+
Prompt
+ {mode === 'design' && (
+
+ {PRESETS.map(p => applyPreset(p)}>{p.name} )}
+
+ )}
+
+
+
+ {mode === 'clone' ? (
+
+
Voice Source
+
+ {/* ── VOICE PROFILES ── */}
+ {profiles.length > 0 && (
+
+
Saved Profiles
+
+ {profiles.map(p => (
+
handleSelectProfile(p)} style={{position:'relative'}}>
+ {p.name}
+ { e.stopPropagation(); handleDeleteProfile(p.id); }}
+ style={{position:'absolute', top:2, right:2, background:'none', border:'none', color:'#fb4934', cursor:'pointer', padding:0}}>
+
+
+
+ ))}
+
+
+ )}
+
+ {!selectedProfile && (
+ <>
+
+
+ {refAudio ? {refAudio.name} : "Select WAV / MP3"}
+
+
{ setRefAudio(e.target.files[0]); setSelectedProfile(null); }} style={{display:'none'}} id="audio-upload" />
+ >
+ )}
+
+ {selectedProfile && (
+
+ Using profile: {profiles.find(p=>p.id===selectedProfile)?.name}
+ setSelectedProfile(null)} style={{marginLeft:8, background:'none', border:'none', color:'#a89984', cursor:'pointer', fontSize:'0.75rem', textDecoration:'underline'}}>clear
+
+ )}
+
+
+
+ {/* Save as profile */}
+ {refAudio && !selectedProfile && (
+
+ {!showSaveProfile ? (
+
setShowSaveProfile(true)} style={{background:'none', border:'1px solid rgba(142,192,124,0.3)', color:'#8ec07c', fontSize:'0.75rem', padding:'4px 10px', borderRadius:6, cursor:'pointer', display:'flex', alignItems:'center', gap:4}}>
+ Save as Voice Profile
+
+ ) : (
+
+ setProfileName(e.target.value)}/>
+ Save
+ setShowSaveProfile(false)} style={{background:'none', border:'none', color:'#a89984', cursor:'pointer', fontSize:'0.75rem'}}>Cancel
+
+ )}
+
+ )}
+
+ ) : (
+
+
Voice Profile
+
+ {Object.entries(CATEGORIES).map(([key, options]) => (
+
+
{key}
+
setVdStates({...vdStates, [key]: e.target.value})}>
+ {options.map(opt => {opt} )}
+
+
+ ))}
+
+
+ )}
+
+
setShowOverrides(!showOverrides)}>
+ Production Overrides
+ {showOverrides ? : }
+
+ {showOverrides && (
+
+
+
+
+
+
+
+
+
+
+ setDenoise(e.target.checked)}/> Denoise
+ setPostprocess(e.target.checked)}/> Postprocess
+
+
+
+ )}
+
+
+ {isGenerating ? : }
+ {isGenerating ? `Synthesizing... (${generationTime}s)` : 'Synthesize Audio'}
+
+ {isGenerating &&
}
+
+ >
+ )}
+
+
+ {/* ── SIDEBAR ── */}
+
+
Activity ({history.length + dubHistory.length})
+
Stored in SQLite · Local disk
+
+ {(history.length + dubHistory.length) === 0 ? (
+
Outputs appear here.
+ ) : (
+ <>
+ {/* Dub history */}
+ {dubHistory.map(item => (
+
restoreDubHistory(item)}>
+
+
+ DUB
+
+
{item.segments_count} segs
+
+
+ {item.filename}
+
+
+
+ {item.language} ({item.language_code})
+
+
+ {Math.round(item.duration)}s
+
+
+ {item.tracks && (
+
Tracks: {JSON.parse(item.tracks || '[]').join(', ')}
+ )}
+
+ ))}
+
+ {/* Clone/Design history */}
+ {history.map(item => (
+
+
+
+ {item.mode === 'clone' ? : } {(item.mode||'').toUpperCase()}
+
+ {item.generation_time &&
{item.generation_time}s
}
+
+ {item.language && item.language !== 'Auto' &&
{item.language}
}
+
{item.text}
+ {item.audio_path &&
}
+ {item.audio_path && (
+
+ Export
+
+ )}
+
+ ))}
+ >
+ )}
+
+ {(history.length + dubHistory.length) > 0 && (
+
{ if (confirm("Clear all history?")) { await fetch(`${API}/history`, {method:'DELETE'}); await loadHistory(); await loadDubHistory(); }}}
+ style={{width:'100%', marginTop:10, padding:5, background:'rgba(251,73,52,0.1)', border:'1px solid rgba(251,73,52,0.3)', borderRadius:6, color:'#fb4934', cursor:'pointer', fontSize:'0.75rem'}}>
+ Clear History
+
+ )}
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png
new file mode 100644
index 00000000..cc51a3d2
Binary files /dev/null and b/frontend/src/assets/hero.png differ
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg
new file mode 100644
index 00000000..5101b674
--- /dev/null
+++ b/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 00000000..9d18b2db
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,328 @@
+@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Inter:wght@400;500;600&display=swap');
+
+:root {
+ /* Gruvbox Dark Theme */
+ --primary: #d3869b; /* Purple/pink accent */
+ --primary-hover: #b16286;
+ --accent: #fabd2f; /* Yellow accent */
+ --bg: #282828; /* Background */
+
+ --glass-bg: rgba(60, 56, 54, 0.7); /* Lighter bg #3c3836 */
+ --glass-border: rgba(235, 219, 178, 0.1);
+ --glass-shadow: 0 4px 20px 0 rgba(29, 32, 33, 0.6);
+ --blur: blur(20px) saturate(140%);
+
+ --text-primary: #ebdbb2; /* Foreground */
+ --text-secondary: #a89984; /* Gray foreground */
+}
+
+@keyframes mesh {
+ 0% { background-position: 0% 50%; }
+ 50% { background-position: 100% 50%; }
+ 100% { background-position: 0% 50%; }
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ background-color: var(--bg);
+ color: var(--text-primary);
+ font-family: 'Inter', sans-serif;
+ overflow-x: hidden;
+ background-image:
+ radial-gradient(circle at 10% 10%, rgba(211, 134, 155, 0.1) 0%, transparent 40%),
+ radial-gradient(circle at 90% 90%, rgba(250, 189, 47, 0.08) 0%, transparent 40%),
+ radial-gradient(circle at 50% 50%, rgba(131, 165, 152, 0.08) 0%, transparent 50%);
+ background-size: 200% 200%;
+ animation: mesh 20s ease-in-out infinite;
+ min-height: 100vh;
+}
+
+h1, h2, h3, h4 { font-family: 'Outfit', sans-serif; margin: 0; }
+
+#root {
+ display: flex;
+ justify-content: center;
+ padding: 1rem;
+}
+
+/* Compact Layouts */
+.app-container {
+ width: 100%;
+ max-width: 1400px;
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 1100px) {
+ .app-container { grid-template-columns: 1fr; }
+}
+
+/* Panels */
+.glass-panel {
+ background: var(--glass-bg);
+ backdrop-filter: var(--blur);
+ -webkit-backdrop-filter: var(--blur);
+ border: 1px solid var(--glass-border);
+ box-shadow: var(--glass-shadow);
+ border-radius: 16px;
+ padding: 16px;
+ position: relative;
+ overflow: hidden;
+ margin-bottom: 1rem;
+}
+
+.glass-panel::before {
+ content: "";
+ position: absolute;
+ top: 0; left: 0; right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.15), rgba(255,255,255,0));
+}
+
+.header-area {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+.header-area h1 {
+ font-size: 1.8rem;
+ font-weight: 800;
+ background: linear-gradient(135deg, #ffffff 0%, #a1a1aa 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+.header-area p { margin: 0; color: var(--text-secondary); font-size: 0.9rem;}
+
+/* Tabs */
+.tabs {
+ display: flex;
+ background: rgba(0,0,0,0.3);
+ padding: 4px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05); margin-bottom: 1rem;
+}
+.tab {
+ flex: 1; padding: 0.5rem; background: transparent; border: none; border-radius: 8px;
+ color: var(--text-secondary); font-weight: 600; font-size: 0.9rem; cursor: pointer;
+ display: flex; align-items: center; justify-content: center; gap: 6px;
+ transition: all 0.2s;
+}
+.tab.active { background: var(--glass-bg); color: white; border: 1px solid rgba(255,255,255,0.1); }
+.tab:not(.active):hover { color: white; background: rgba(255,255,255,0.05); }
+
+/* Grid systems */
+.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px;}
+.grid-4 { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;}
+
+.label-row {
+ display: flex; align-items: center; gap: 6px; margin-bottom: 4px;
+ color: var(--text-primary); font-size: 0.85rem; font-weight: 500;
+}
+.label-icon { color: #a78bfa; }
+
+/* Inputs */
+.input-base {
+ width: 100%; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 8px; padding: 0.6rem; color: white; font-family: 'Inter', sans-serif;
+ font-size: 0.9rem; transition: all 0.2s; box-sizing: border-box;
+}
+.input-base:focus { outline: none; border-color: var(--primary); }
+textarea.input-base { min-height: 100px; resize: vertical; }
+
+select.input-base {
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23a1a1aa%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.8rem top 50%;
+ background-size: 0.5rem auto;
+}
+
+/* Range Inputs */
+input[type="range"] {
+ -webkit-appearance: none; width: 100%; height: 4px;
+ background: rgba(255,255,255,0.1); border-radius: 2px; outline: none; margin-top: 6px;
+}
+input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
+ background: var(--text-primary); cursor: pointer; transition: transform 0.1s;
+}
+input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
+.val-bubble { font-size: 0.8rem; background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; }
+
+/* Tags */
+.tags-container { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
+.tag-btn {
+ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
+ color: var(--text-secondary); padding: 4px 8px; border-radius: 6px;
+ font-size: 0.75rem; cursor: pointer; transition: all 0.2s; white-space: nowrap;
+}
+.tag-btn:hover { background: rgba(255,255,255,0.1); color: white; border-color: rgba(255,255,255,0.2); }
+
+.file-drag {
+ border: 1px dashed rgba(255,255,255,0.2); border-radius: 8px; padding: 16px;
+ text-align: center; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 8px;
+ background: rgba(0,0,0,0.1);
+}
+.file-drag:hover { border-color: var(--primary); background: rgba(94, 106, 210, 0.05); }
+.file-drag p { margin: 0; font-size: 0.85rem; color: var(--text-secondary); }
+
+/* Progress Overrides */
+.progress-container {
+ width: 100%;
+ background: rgba(0,0,0,0.3);
+ border-radius: 6px;
+ height: 6px;
+ overflow: hidden;
+ margin-top: 8px;
+}
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--primary), var(--accent));
+ transition: width 0.3s ease;
+}
+
+/* Button */
+.btn-primary {
+ width: 100%; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%);
+ color: var(--bg); border: 1px solid rgba(255,255,255,0.1); padding: 12px;
+ border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer;
+ display: flex; justify-content: center; align-items: center; gap: 8px;
+ margin-top: 1rem; transition: all 0.2s; box-shadow: 0 4px 12px rgba(94, 106, 210, 0.3);
+}
+.btn-primary:hover:not(:disabled) { transform: translateY(-1px); }
+.btn-primary:active:not(:disabled) { transform: translateY(1px); }
+.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
+.spinner { animation: spin 1s linear infinite; }
+
+/* History */
+.history-panel {
+ max-height: calc(100vh - 2rem);
+ overflow-y: auto;
+}
+.history-item {
+ background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.05);
+ border-radius: 8px; padding: 12px; margin-bottom: 12px;
+}
+.history-header {
+ display: flex; justify-content: space-between; margin-bottom: 6px; align-items: center;
+}
+.history-badge {
+ display: inline-flex; align-items: center; gap: 4px;
+ background: rgba(94, 106, 210, 0.15); color: #a78bfa; padding: 2px 6px;
+ border-radius: 4px; border: 1px solid rgba(94, 106, 210, 0.3); font-size: 0.65rem; font-weight: 600;
+}
+.history-time { font-size: 0.7rem; color: #4ade80; font-family: monospace;}
+.history-text {
+ font-size: 0.75rem;
+ color: var(--text-primary);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.clickable-history {
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.clickable-history:hover {
+ background: rgba(250, 189, 47, 0.05); /* Yellow tint on hover */
+ border-color: rgba(250, 189, 47, 0.3);
+}
+audio { height: 28px; width: 100%; outline: none; border-radius: 6px; }
+
+/* Override / Modals */
+.override-toggle {
+ display: flex; align-items: center; justify-content: space-between;
+ background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: 8px;
+ font-size: 0.85rem; color: var(--text-primary); cursor: pointer; border: 1px solid rgba(255,255,255,0.05);
+ margin-top: 1rem;
+}
+.override-toggle:hover { background: rgba(0,0,0,0.3); }
+.override-content {
+ background: rgba(0,0,0,0.1); border: 1px solid rgba(255,255,255,0.05); border-top: none;
+ padding: 12px; border-radius: 0 0 8px 8px; margin-bottom: 1rem;
+}
+.preset-grid {
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; margin-bottom: 12px;
+}
+.preset-btn {
+ background: rgba(94, 106, 210, 0.1); border: 1px solid rgba(94, 106, 210, 0.3);
+ color: #c7d2fe; padding: 6px; border-radius: 6px; font-size: 0.75rem;
+ cursor: pointer; transition: all 0.2s; text-align: left;
+ display: flex; align-items: center; gap: 4px;
+}
+.preset-btn:hover { background: rgba(94, 106, 210, 0.2); border-color: #a78bfa; }
+
+/* ═══ VIDEO DUBBING SEGMENT TABLE ═══ */
+.segment-table {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid rgba(255,255,255,0.05);
+ border-radius: 8px;
+ margin-top: 8px;
+}
+.segment-header {
+ display: flex; align-items: center; gap: 8px;
+ padding: 6px 10px;
+ background: rgba(0,0,0,0.3);
+ font-size: 0.7rem; font-weight: 600; color: var(--text-secondary);
+ position: sticky; top: 0; z-index: 1;
+ border-bottom: 1px solid rgba(255,255,255,0.05);
+}
+.segment-row {
+ display: flex; align-items: center; gap: 8px;
+ padding: 4px 10px;
+ border-bottom: 1px solid rgba(255,255,255,0.03);
+ transition: background 0.3s;
+}
+.segment-row:hover { background: rgba(255,255,255,0.02); }
+.segment-row.segment-active {
+ background: rgba(211, 134, 155, 0.12);
+ border-left: 3px solid var(--primary);
+}
+.segment-row.segment-done {
+ opacity: 0.6;
+}
+.segment-time {
+ width: 80px; flex-shrink: 0;
+ font-size: 0.7rem; font-family: monospace; color: #83a598;
+}
+.segment-input {
+ flex: 1;
+ padding: 4px 8px !important;
+ font-size: 0.8rem !important;
+ border-radius: 4px !important;
+ min-height: auto !important;
+}
+.segment-del {
+ width: 28px; height: 28px; flex-shrink: 0;
+ background: none; border: 1px solid rgba(251,73,52,0.2);
+ color: #fb4934; border-radius: 4px; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: all 0.2s;
+}
+.segment-del:hover { background: rgba(251,73,52,0.15); }
+.segment-del:disabled { opacity: 0.3; cursor: not-allowed; }
+
+/* Voice profile active state */
+.profile-active {
+ background: rgba(142, 192, 124, 0.15) !important;
+ border-color: #8ec07c !important;
+ color: #8ec07c !important;
+}
+
+/* Segment preview play button */
+.segment-play {
+ width: 28px; height: 28px; flex-shrink: 0;
+ background: rgba(131,165,152,0.15); border: 1px solid rgba(131,165,152,0.3);
+ color: #83a598; border-radius: 4px; cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ transition: all 0.2s;
+}
+.segment-play:hover { background: rgba(131,165,152,0.3); color: #8ec07c; }
diff --git a/frontend/src/languages.json b/frontend/src/languages.json
new file mode 100644
index 00000000..704adf97
--- /dev/null
+++ b/frontend/src/languages.json
@@ -0,0 +1 @@
+["Auto", "Abadi", "Abkhazian", "Abron", "Abua", "Adamawa Fulfulde", "Adyghe", "Afade", "Afrikaans", "Agwagwune", "Aja (Benin)", "Akebu", "Alago", "Albanian", "Algerian Arabic", "Algerian Saharan Arabic", "Ambo-Pasco Quechua", "Ambonese Malay", "Amdo Tibetan", "Amharic", "Anaang", "Angika", "Antankarana Malagasy", "Aragonese", "Arb\u00ebresh\u00eb Albanian", "Arequipa-La Uni\u00f3n Quechua", "Armenian", "Ashe", "Ash\u00e9ninka Peren\u00e9", "Askopan", "Assamese", "Asturian", "Atayal", "Awak", "Ayacucho Quechua", "Azerbaijani", "Baatonum", "Bacama", "Bade", "Bafia", "Bafut", "Bagirmi Fulfulde", "Bago-Kusuntu", "Baharna Arabic", "Bakoko", "Balanta-Ganja", "Balti", "Bamenyam", "Bamun", "Bangwinji", "Banjar", "Bankon", "Baoul\u00e9", "Bara Malagasy", "Barok", "Basa (Cameroon)", "Basa (Nigeria)", "Bashkir", "Basque", "Batak Mandailing", "Batanga", "Bateri", "Bats", "Bayot", "Bebele", "Belarusian", "Bengali", "Betawi", "Bhili", "Bhojpuri", "Bilur", "Bima", "Bodo", "Boghom", "Bokyi", "Bomu", "Bondei", "Borgu Fulfulde", "Bosnian", "Brahui", "Braj", "Breton", "Buduma", "Buginese", "Bukharic", "Bulgarian", "Bulu (Cameroon)", "Bundeli", "Bunun", "Bura-Pabir", "Burak", "Burmese", "Burushaski", "Cacaloxtepec Mixtec", "Cajatambo North Lima Quechua", "Cakfem-Mushere", "Cameroon Pidgin", "Campidanese Sardinian", "Cantonese", "Catalan", "Cebuano", "Cen", "Central Kurdish", "Central Nahuatl", "Central Pame", "Central Pashto", "Central Puebla Nahuatl", "Central Tarahumara", "Central Yupik", "Central-Eastern Niger Fulfulde", "Chadian Arabic", "Chichewa", "Chichicapan Zapotec", "Chiga", "Chimalapa Zoque", "Chimborazo Highland Quichua", "Chinese", "Chiqui\u00e1n Ancash Quechua", "Chitwania Tharu", "Chokwe", "Chuvash", "Cibak", "Coastal Konjo", "Copainal\u00e1 Zoque", "Cornish", "Corongo Ancash Quechua", "Croatian", "Cross River Mbembe", "Cuyamecalco Mixtec", "Czech", "Dadiya", "Dagbani", "Dameli", "Danish", "Dargwa", "Dazaga", "Deccan", "Degema", "Dera (Nigeria)", "Dghwede", "Dhatki", "Dhivehi", "Dhofari Arabic", "Dijim-Bwilim", "Dogri", "Domaaki", "Dotyali", "Duala", "Dutch", "Du\u0303ya", "Dyula", "Eastern Balochi", "Eastern Bolivian Guaran\u00ed", "Eastern Egyptian Bedawi Arabic", "Eastern Krahn", "Eastern Mari", "Eastern Yiddish", "Ebri\u00e9", "Eggon", "Egyptian Arabic", "Ejagham", "Eleme", "Eloyi", "Embu", "English", "Erzya", "Esan", "Esperanto", "Estonian", "Eton (Cameroon)", "Ewondo", "Extremaduran", "Fang (Equatorial Guinea)", "Fanti", "Farefare", "Fe'fe'", "Filipino", "Filomena Mata-Coahuitl\u00e1n Totonac", "Finnish", "Fipa", "French", "Fulah", "Galician", "Gambian Wolof", "Ganda", "Garhwali", "Gawar-Bati", "Gawri", "Gbagyi", "Gbari", "Geji", "Gen", "Georgian", "German", "Geser-Gorom", "Gheg Albanian", "Ghom\u00e1l\u00e1'", "Gidar", "Glavda", "Goan Konkani", "Goaria", "Goemai", "Gola", "Greek", "Guarani", "Guduf-Gava", "Guerrero Amuzgo", "Gujarati", "Gujari", "Gulf Arabic", "Gurgula", "Gusii", "Gusilay", "Gweno", "G\u00fcil\u00e1 Zapotec", "Hadothi", "Hahon", "Haitian", "Hakha Chin", "Hak\u00f6", "Halia", "Hausa", "Hawaiian", "Hazaragi", "Hebrew", "Hemba", "Herero", "Highland Konjo", "Hijazi Arabic", "Hindi", "Huarijio", "Huautla Mazatec", "Huaxcaleca Nahuatl", "Huba", "Huitepec Mixtec", "Hula", "Hungarian", "Hunjara-Kaina Ke", "Hwana", "Ibibio", "Icelandic", "Idakho-Isukha-Tiriki", "Idoma", "Igbo", "Igo", "Ikposo", "Ikwere", "Imbabura Highland Quichua", "Indonesian", "Indus Kohistani", "Interlingua (International Auxiliary Language Association)", "Inupiaq", "Irish", "Iron Ossetic", "Isekiri", "Isoko", "Italian", "Ito", "Itz\u00e1", "Ixtayutla Mixtec", "Izon", "Jambi Malay", "Japanese", "Jaqaru", "Jauja Wanca Quechua", "Jaunsari", "Javanese", "Jiba", "Jju", "Judeo-Moroccan Arabic", "Juxtlahuaca Mixtec", "Kabardian", "Kabras", "Kabuverdianu", "Kabyle", "Kachi Koli", "Kairak", "Kalabari", "Kalasha", "Kalenjin", "Kalkoti", "Kamba", "Kamo", "Kanauji", "Kanembu", "Kannada", "Karekare", "Kashmiri", "Kathoriya Tharu", "Kati", "Kazakh", "Keiyo", "Khams Tibetan", "Khana", "Khetrani", "Khmer", "Khowar", "Kinga", "Kinnauri", "Kinyarwanda", "Kirghiz", "Kirya-Konz\u0259l", "Kochila Tharu", "Kohistani Shina", "Kohumono", "Kok Borok", "Kol (Papua New Guinea)", "Kom (Cameroon)", "Koma", "Konkani", "Konzo", "Korean", "Korwa", "Kota (India)", "Koti", "Kuanua", "Kuanyama", "Kui (India)", "Kulung (Nigeria)", "Kuot", "Kushi", "Kwambi", "Kwasio", "Lala-Roba", "Lamang", "Lao", "Larike-Wakasihu", "Lasi", "Latgalian", "Latvian", "Levantine Arabic", "Liana-Seti", "Liberia Kpelle", "Liberian English", "Libyan Arabic", "Ligurian", "Lijili", "Lingala", "Lithuanian", "Loarki", "Logooli", "Logudorese Sardinian", "Loja Highland Quichua", "Loloda", "Longuda", "Loxicha Zapotec", "Luba-Lulua", "Luo", "Lushai", "Luxembourgish", "Maasina Fulfulde", "Maba (Chad)", "Macedo-Romanian", "Macedonian", "Mada (Cameroon)", "Mafa", "Maithili", "Malay", "Malayalam", "Mali", "Malinaltepec Me'phaa", "Maltese", "Mandara", "Mandjak", "Manggarai", "Manipuri", "Mansoanka", "Manx", "Maori", "Marathi", "Marghi Central", "Marghi South", "Maria (India)", "Marwari (Pakistan)", "Masana", "Masikoro Malagasy", "Mats\u00e9s", "Mazaltepec Zapotec", "Mazatl\u00e1n Mazatec", "Mazatl\u00e1n Mixe", "Mbe", "Mbo (Cameroon)", "Mbum", "Medumba", "Mekeo", "Meru", "Mesopotamian Arabic", "Mewari", "Min Nan Chinese", "Mingrelian", "Mitlatongo Mixtec", "Miya", "Mokpwe", "Moksha", "Mom Jango", "Mongolian", "Moroccan Arabic", "Motu", "Mpiemo", "Mpumpong", "Mundang", "Mungaka", "Musey", "Musgu", "Musi", "Naba", "Najdi Arabic", "Nalik", "Nawdm", "Ndonga", "Neapolitan", "Nepali", "Ngamo", "Ngas", "Ngiemboon", "Ngizim", "Ngomba", "Ngombale", "Nigerian Fulfulde", "Nigerian Pidgin", "Nimadi", "Nobiin", "North Mesopotamian Arabic", "North Moluccan Malay", "Northern Betsimisaraka Malagasy", "Northern Hindko", "Northern Kurdish", "Northern Pame", "Northern Pashto", "Northern Uzbek", "Northwest Gbaya", "Norwegian", "Norwegian Bokm\u00e5l", "Norwegian Nynorsk", "Notsi", "Nyankpa", "Nyungwe", "Nzanyi", "N\u00fcpode Huitoto", "Occitan", "Od", "Odia", "Odual", "Omani Arabic", "Orizaba Nahuatl", "Orma", "Ormuri", "Oromo", "Pahari-Potwari", "Paiwan", "Panjabi", "Papuan Malay", "Parkari Koli", "Pedi", "Pero", "Persian", "Petats", "Phalura", "Piemontese", "Piya-Kwonci", "Plateau Malagasy", "Polish", "Poqomam", "Portuguese", "Pulaar", "Pular", "Puno Quechua", "Pushto", "P\u00f6koot", "Qaqet", "Quiotepec Chinantec", "Rana Tharu", "Rangi", "Rapoisi", "Ratahan", "Ray\u00f3n Zoque", "Romanian", "Romansh", "Rombo", "Rotokas", "Rukai", "Russian", "Sacapulteco", "Saidi Arabic", "Sakalava Malagasy", "Sakizaya", "Saleman", "Samba Daka", "Samba Leko", "San Felipe Otlaltepec Popoloca", "San Francisco Del Mar Huave", "San Juan Atzingo Popoloca", "San Mart\u00edn Itunyoso Triqui", "San Miguel El Grande Mixtec", "Sansi", "Sanskrit", "Santa Ana de Tusi Pasco Quechua", "Santa Catarina Albarradas Zapotec", "Santali", "Santiago del Estero Quichua", "Saposa", "Saraiki", "Sardinian", "Saya", "Sediq", "Serbian", "Seri", "Shina", "Shona", "Siar-Lak", "Sibe", "Sicilian", "Sihuas Ancash Quechua", "Sikkimese", "Sinaugoro", "Sindhi", "Sindhi Bhil", "Sinhala", "Sinicahua Mixtec", "Sipacapense", "Siwai", "Slovak", "Slovenian", "Solos", "Somali", "Soninke", "South Giziga", "South Ucayali Ash\u00e9ninka", "Southeastern Nochixtl\u00e1n Mixtec", "Southern Betsimisaraka Malagasy", "Southern Pashto", "Southern Pastaza Quechua", "Soyaltepec Mazatec", "Spanish", "Standard Arabic", "Standard Moroccan Tamazight", "Sudanese Arabic", "Sulka", "Svan", "Swahili", "Swedish", "Tae'", "Tahaggart Tamahaq", "Taita", "Tajik", "Tamil", "Tandroy-Mahafaly Malagasy", "Tangale", "Tanosy Malagasy", "Tarok", "Tatar", "Tedaga", "Telugu", "Tem", "Teop", "Tepeuxila Cuicatec", "Tepinapa Chinantec", "Tera", "Terei", "Termanu", "Tesaka Malagasy", "Tetelcingo Nahuatl", "Teutila Cuicatec", "Thai", "Tibetan", "Tida\u00e1 Mixtec", "Tidore", "Tigak", "Tigre", "Tigrinya", "Tilquiapan Zapotec", "Tinputz", "Tlacoapa Me'phaa", "Tlacoatzintepec Chinantec", "Tlingit", "Toki Pona", "Tomoip", "Tondano", "Tonsea", "Tooro", "Torau", "Torwali", "Tsimihety Malagasy", "Tsotso", "Tswana", "Tugen", "Tuki", "Tula", "Tulu", "Tunen", "Tungag", "Tunisian Arabic", "Tupuri", "Turkana", "Turkish", "Turkmen", "Tututepec Mixtec", "Twi", "Ubaghara", "Uighur", "Ukrainian", "Umbundu", "Upper Sorbian", "Urdu", "Ushojo", "Uzbek", "Vai", "Vietnamese", "Votic", "V\u00f5ro", "Waci Gbe", "Wadiyara Koli", "Waja", "Wakhi", "Wanga", "Wapan", "Warji", "Welsh", "Wemale", "Western Frisian", "Western Highland Purepecha", "Western Juxtlahuaca Mixtec", "Western Maninkakan", "Western Mari", "Western Niger Fulfulde", "Western Panjabi", "Wolof", "Wuzlam", "Xanagu\u00eda Zapotec", "Xhosa", "Yace", "Yakut", "Yalahatan", "Yanahuanca Pasco Quechua", "Yangben", "Yaqui", "Yauyos Quechua", "Yekhee", "Yiddish", "Yidgha", "Yoruba", "Yutanduchi Mixtec", "Zacatl\u00e1n-Ahuacatl\u00e1n-Tepetzintla Nahuatl", "Zarma", "Zaza", "Zulu", "\u00d6mie"]
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 00000000..b9a1a6de
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 00000000..8b0f57b9
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/get_langs.py b/get_langs.py
new file mode 100644
index 00000000..34c0efe1
--- /dev/null
+++ b/get_langs.py
@@ -0,0 +1,5 @@
+import json
+from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name
+languages = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES)
+with open("frontend/src/languages.json", "w") as f:
+ json.dump(languages, f)
diff --git a/omnivoice/__init__.py b/omnivoice/__init__.py
new file mode 100644
index 00000000..fb755bf7
--- /dev/null
+++ b/omnivoice/__init__.py
@@ -0,0 +1,28 @@
+import warnings
+from importlib.metadata import PackageNotFoundError, version
+
+warnings.filterwarnings("ignore", module="torchaudio")
+warnings.filterwarnings(
+ "ignore",
+ category=SyntaxWarning,
+ message="invalid escape sequence",
+ module="pydub.utils",
+)
+warnings.filterwarnings(
+ "ignore",
+ category=FutureWarning,
+ module="torch.distributed.algorithms.ddp_comm_hooks",
+)
+
+try:
+ __version__ = version("omnivoice")
+except PackageNotFoundError:
+ __version__ = "0.0.0"
+
+from omnivoice.models.omnivoice import (
+ OmniVoice,
+ OmniVoiceConfig,
+ OmniVoiceGenerationConfig,
+)
+
+__all__ = ["OmniVoice", "OmniVoiceConfig", "OmniVoiceGenerationConfig"]
diff --git a/omnivoice/cli/__init__.py b/omnivoice/cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/cli/demo.py b/omnivoice/cli/demo.py
new file mode 100644
index 00000000..73dd0a97
--- /dev/null
+++ b/omnivoice/cli/demo.py
@@ -0,0 +1,542 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Gradio demo for OmniVoice.
+
+Supports voice cloning and voice design.
+
+Usage:
+ omnivoice-demo --model /path/to/checkpoint --port 8000
+"""
+
+import argparse
+import logging
+from typing import Any, Dict
+
+import gradio as gr
+import numpy as np
+import torch
+
+from omnivoice import OmniVoice, OmniVoiceGenerationConfig
+from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name
+
+
+def get_best_device():
+ """Auto-detect the best available device: CUDA > MPS > CPU."""
+ if torch.cuda.is_available():
+ return "cuda"
+ if torch.backends.mps.is_available():
+ return "mps"
+ return "cpu"
+
+
+# ---------------------------------------------------------------------------
+# Language list — all 600+ supported languages
+# ---------------------------------------------------------------------------
+_ALL_LANGUAGES = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES)
+
+
+# ---------------------------------------------------------------------------
+# Voice Design instruction templates
+# ---------------------------------------------------------------------------
+# Each option is displayed as "English / 中文".
+# The model expects English for accents and Chinese for dialects.
+_CATEGORIES = {
+ "Gender / 性别": ["Male / 男", "Female / 女"],
+ "Age / 年龄": [
+ "Child / 儿童",
+ "Teenager / 少年",
+ "Young Adult / 青年",
+ "Middle-aged / 中年",
+ "Elderly / 老年",
+ ],
+ "Pitch / 音调": [
+ "Very Low Pitch / 极低音调",
+ "Low Pitch / 低音调",
+ "Moderate Pitch / 中音调",
+ "High Pitch / 高音调",
+ "Very High Pitch / 极高音调",
+ ],
+ "Style / 风格": ["Whisper / 耳语"],
+ "English Accent / 英文口音": [
+ "American Accent / 美式口音",
+ "Australian Accent / 澳大利亚口音",
+ "British Accent / 英国口音",
+ "Chinese Accent / 中国口音",
+ "Canadian Accent / 加拿大口音",
+ "Indian Accent / 印度口音",
+ "Korean Accent / 韩国口音",
+ "Portuguese Accent / 葡萄牙口音",
+ "Russian Accent / 俄罗斯口音",
+ "Japanese Accent / 日本口音",
+ ],
+ "Chinese Dialect / 中文方言": [
+ "Henan Dialect / 河南话",
+ "Shaanxi Dialect / 陕西话",
+ "Sichuan Dialect / 四川话",
+ "Guizhou Dialect / 贵州话",
+ "Yunnan Dialect / 云南话",
+ "Guilin Dialect / 桂林话",
+ "Jinan Dialect / 济南话",
+ "Shijiazhuang Dialect / 石家庄话",
+ "Gansu Dialect / 甘肃话",
+ "Ningxia Dialect / 宁夏话",
+ "Qingdao Dialect / 青岛话",
+ "Northeast Dialect / 东北话",
+ ],
+}
+
+_ATTR_INFO = {
+ "English Accent / 英文口音": "Only effective for English speech.",
+ "Chinese Dialect / 中文方言": "Only effective for Chinese speech.",
+}
+
+# ---------------------------------------------------------------------------
+# Argument parser
+# ---------------------------------------------------------------------------
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ prog="omnivoice-demo",
+ description="Launch a Gradio demo for OmniVoice.",
+ formatter_class=argparse.RawTextHelpFormatter,
+ )
+ parser.add_argument(
+ "--model",
+ default="k2-fsa/OmniVoice",
+ help="Model checkpoint path or HuggingFace repo id.",
+ )
+ parser.add_argument(
+ "--device", default=None, help="Device to use. Auto-detected if not specified."
+ )
+ parser.add_argument("--ip", default="0.0.0.0", help="Server IP (default: 0.0.0.0).")
+ parser.add_argument(
+ "--port", type=int, default=7860, help="Server port (default: 7860)."
+ )
+ parser.add_argument(
+ "--root-path",
+ default=None,
+ help="Root path for reverse proxy.",
+ )
+ parser.add_argument(
+ "--share", action="store_true", default=False, help="Create public link."
+ )
+ parser.add_argument(
+ "--no-asr",
+ action="store_true",
+ default=False,
+ help="Skip loading Whisper ASR model. Reference text auto-transcription"
+ " will be unavailable.",
+ )
+ return parser
+
+
+# ---------------------------------------------------------------------------
+# Build demo
+# ---------------------------------------------------------------------------
+
+
+def build_demo(
+ model: OmniVoice,
+ checkpoint: str,
+ generate_fn=None,
+) -> gr.Blocks:
+
+ sampling_rate = model.sampling_rate
+
+ # -- shared generation core --
+ def _gen_core(
+ text,
+ language,
+ ref_audio,
+ instruct,
+ num_step,
+ guidance_scale,
+ denoise,
+ speed,
+ duration,
+ preprocess_prompt,
+ postprocess_output,
+ mode,
+ ref_text=None,
+ ):
+ if not text or not text.strip():
+ return None, "Please enter the text to synthesize."
+
+ gen_config = OmniVoiceGenerationConfig(
+ num_step=int(num_step or 32),
+ guidance_scale=float(guidance_scale) if guidance_scale is not None else 2.0,
+ denoise=bool(denoise) if denoise is not None else True,
+ preprocess_prompt=bool(preprocess_prompt),
+ postprocess_output=bool(postprocess_output),
+ )
+
+ lang = language if (language and language != "Auto") else None
+
+ kw: Dict[str, Any] = dict(
+ text=text.strip(), language=lang, generation_config=gen_config
+ )
+
+ if speed is not None and float(speed) != 1.0:
+ kw["speed"] = float(speed)
+ if duration is not None and float(duration) > 0:
+ kw["duration"] = float(duration)
+
+ if mode == "clone":
+ if not ref_audio:
+ return None, "Please upload a reference audio."
+ kw["voice_clone_prompt"] = model.create_voice_clone_prompt(
+ ref_audio=ref_audio,
+ ref_text=ref_text,
+ )
+
+ if instruct and instruct.strip():
+ kw["instruct"] = instruct.strip()
+
+ try:
+ audio = model.generate(**kw)
+ except Exception as e:
+ return None, f"Error: {type(e).__name__}: {e}"
+
+ waveform = audio[0].squeeze(0).numpy() # (T,)
+ waveform = (waveform * 32767).astype(np.int16)
+ return (sampling_rate, waveform), "Done."
+
+ # Allow external wrappers (e.g. spaces.GPU for ZeroGPU Spaces)
+ _gen = generate_fn if generate_fn is not None else _gen_core
+
+ # =====================================================================
+ # UI
+ # =====================================================================
+ theme = gr.themes.Soft(
+ font=["Inter", "Arial", "sans-serif"],
+ )
+ css = """
+ .gradio-container {max-width: 100% !important; font-size: 16px !important;}
+ .gradio-container h1 {font-size: 1.5em !important;}
+ .gradio-container .prose {font-size: 1.1em !important;}
+ .compact-audio audio {height: 60px !important;}
+ .compact-audio .waveform {min-height: 80px !important;}
+ """
+
+ # Reusable: language dropdown component
+ def _lang_dropdown(label="Language (optional) / 语种 (可选)", value="Auto"):
+ return gr.Dropdown(
+ label=label,
+ choices=_ALL_LANGUAGES,
+ value=value,
+ allow_custom_value=False,
+ interactive=True,
+ info="Keep as Auto to auto-detect the language.",
+ )
+
+ # Reusable: optional generation settings accordion
+ def _gen_settings():
+ with gr.Accordion("Generation Settings (optional)", open=False):
+ sp = gr.Slider(
+ 0.5,
+ 1.5,
+ value=1.0,
+ step=0.05,
+ label="Speed",
+ info="1.0 = normal. >1 faster, <1 slower. Ignored if Duration is set.",
+ )
+ du = gr.Number(
+ value=None,
+ label="Duration (seconds)",
+ info=(
+ "Leave empty to use speed."
+ " Set a fixed duration to override speed."
+ ),
+ )
+ ns = gr.Slider(
+ 4,
+ 64,
+ value=32,
+ step=1,
+ label="Inference Steps",
+ info="Default: 32. Lower = faster, higher = better quality.",
+ )
+ dn = gr.Checkbox(
+ label="Denoise",
+ value=True,
+ info="Default: enabled. Uncheck to disable denoising.",
+ )
+ gs = gr.Slider(
+ 0.0,
+ 4.0,
+ value=2.0,
+ step=0.1,
+ label="Guidance Scale (CFG)",
+ info="Default: 2.0.",
+ )
+ pp = gr.Checkbox(
+ label="Preprocess Prompt",
+ value=True,
+ info="apply silence removal and trimming to the reference "
+ "audio, add punctuation in the end of reference text (if not already)",
+ )
+ po = gr.Checkbox(
+ label="Postprocess Output",
+ value=True,
+ info="Remove long silences from generated audio.",
+ )
+ return ns, gs, dn, sp, du, pp, po
+
+ with gr.Blocks(theme=theme, css=css, title="OmniVoice Demo") as demo:
+ gr.Markdown(
+ """
+# OmniVoice Demo
+
+State-of-the-art text-to-speech model for **600+ languages**, supporting:
+
+- **Voice Clone** — Clone any voice from a reference audio
+- **Voice Design** — Create custom voices with speaker attributes
+
+Built with [OmniVoice](https://github.com/k2-fsa/OmniVoice)
+by Xiaomi AI Lab Next-gen Kaldi team.
+"""
+ )
+
+ with gr.Tabs():
+ # ==============================================================
+ # Voice Clone
+ # ==============================================================
+ with gr.TabItem("Voice Clone"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vc_text = gr.Textbox(
+ label="Text to Synthesize / 待合成文本",
+ lines=4,
+ placeholder="Enter the text you want to synthesize...",
+ )
+ vc_ref_audio = gr.Audio(
+ label="Reference Audio / 参考音频",
+ type="filepath",
+ elem_classes="compact-audio",
+ )
+ gr.Markdown(
+ ""
+ "Recommended: 3–10 seconds audio. "
+ " "
+ )
+ vc_ref_text = gr.Textbox(
+ label=("Reference Text (optional)" " / 参考音频文本(可选)"),
+ lines=2,
+ placeholder="Transcript of the reference audio. Leave empty"
+ " to auto-transcribe via ASR models.",
+ )
+ vc_lang = _lang_dropdown("Language (optional) / 语种 (可选)")
+ with gr.Accordion("Instruct (optional)", open=False):
+ vc_instruct = gr.Textbox(label="Instruct", lines=2)
+ (
+ vc_ns,
+ vc_gs,
+ vc_dn,
+ vc_sp,
+ vc_du,
+ vc_pp,
+ vc_po,
+ ) = _gen_settings()
+ vc_btn = gr.Button("Generate / 生成", variant="primary")
+ with gr.Column(scale=1):
+ vc_audio = gr.Audio(
+ label="Output Audio / 合成结果",
+ type="numpy",
+ )
+ vc_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def _clone_fn(
+ text, lang, ref_aud, ref_text, instruct, ns, gs, dn, sp, du, pp, po
+ ):
+ return _gen(
+ text,
+ lang,
+ ref_aud,
+ instruct,
+ ns,
+ gs,
+ dn,
+ sp,
+ du,
+ pp,
+ po,
+ mode="clone",
+ ref_text=ref_text or None,
+ )
+
+ vc_btn.click(
+ _clone_fn,
+ inputs=[
+ vc_text,
+ vc_lang,
+ vc_ref_audio,
+ vc_ref_text,
+ vc_instruct,
+ vc_ns,
+ vc_gs,
+ vc_dn,
+ vc_sp,
+ vc_du,
+ vc_pp,
+ vc_po,
+ ],
+ outputs=[vc_audio, vc_status],
+ )
+
+ # ==============================================================
+ # Voice Design
+ # ==============================================================
+ with gr.TabItem("Voice Design"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vd_text = gr.Textbox(
+ label="Text to Synthesize / 待合成文本",
+ lines=4,
+ placeholder="Enter the text you want to synthesize...",
+ )
+ vd_lang = _lang_dropdown()
+
+ _AUTO = "Auto"
+ vd_groups = []
+ for _cat, _choices in _CATEGORIES.items():
+ vd_groups.append(
+ gr.Dropdown(
+ label=_cat,
+ choices=[_AUTO] + _choices,
+ value=_AUTO,
+ info=_ATTR_INFO.get(_cat),
+ )
+ )
+
+ (
+ vd_ns,
+ vd_gs,
+ vd_dn,
+ vd_sp,
+ vd_du,
+ vd_pp,
+ vd_po,
+ ) = _gen_settings()
+ vd_btn = gr.Button("Generate / 生成", variant="primary")
+ with gr.Column(scale=1):
+ vd_audio = gr.Audio(
+ label="Output Audio / 合成结果",
+ type="numpy",
+ )
+ vd_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def _build_instruct(groups):
+ """Extract instruct text from UI dropdowns.
+
+ Language unification and validation is handled by
+ _resolve_instruct inside _preprocess_all.
+ """
+ selected = [g for g in groups if g and g != "Auto"]
+ if not selected:
+ return None
+ parts = []
+ for v in selected:
+ if " / " in v:
+ en, zh = v.split(" / ", 1)
+ # Dialects have no English equivalent
+ if "Dialect" in v.split(" / ")[0]:
+ parts.append(zh.strip())
+ else:
+ parts.append(en.strip())
+ else:
+ parts.append(v)
+ return ", ".join(parts)
+
+ def _design_fn(text, lang, ns, gs, dn, sp, du, pp, po, *groups):
+ return _gen(
+ text,
+ lang,
+ None,
+ _build_instruct(groups),
+ ns,
+ gs,
+ dn,
+ sp,
+ du,
+ pp,
+ po,
+ mode="design",
+ )
+
+ vd_btn.click(
+ _design_fn,
+ inputs=[
+ vd_text,
+ vd_lang,
+ vd_ns,
+ vd_gs,
+ vd_dn,
+ vd_sp,
+ vd_du,
+ vd_pp,
+ vd_po,
+ ]
+ + vd_groups,
+ outputs=[vd_audio, vd_status],
+ )
+
+ return demo
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+
+def main(argv=None) -> int:
+ logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s %(name)s %(levelname)s: %(message)s",
+ )
+ parser = build_parser()
+ args = parser.parse_args(argv)
+
+ device = args.device or get_best_device()
+
+ checkpoint = args.model
+ if not checkpoint:
+ parser.print_help()
+ return 0
+ logging.info(f"Loading model from {checkpoint}, device={device} ...")
+ model = OmniVoice.from_pretrained(
+ checkpoint,
+ device_map=device,
+ dtype=torch.float16,
+ load_asr=not args.no_asr,
+ )
+ print("Model loaded.")
+
+ demo = build_demo(model, checkpoint)
+
+ demo.queue().launch(
+ server_name=args.ip,
+ server_port=args.port,
+ share=args.share,
+ root_path=args.root_path,
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/omnivoice/cli/infer.py b/omnivoice/cli/infer.py
new file mode 100644
index 00000000..f913304e
--- /dev/null
+++ b/omnivoice/cli/infer.py
@@ -0,0 +1,157 @@
+"""Single-item inference CLI for OmniVoice.
+
+Generates audio from a single text input using voice cloning,
+voice design, or auto voice.
+
+Usage:
+ # Voice cloning
+ omnivoice-infer --model k2-fsa/OmniVoice \
+ --text "Hello, this is a text for text-to-speech." \
+ --ref_audio ref.wav --ref_text "Reference transcript." --output out.wav
+
+ # Voice design
+ omnivoice-infer --model k2-fsa/OmniVoice \
+ --text "Hello, this is a text for text-to-speech." \
+ --instruct "male, British accent" --output out.wav
+
+ # Auto voice
+ omnivoice-infer --model k2-fsa/OmniVoice \
+ --text "Hello, this is a text for text-to-speech." --output out.wav
+"""
+
+import argparse
+import logging
+
+import torch
+import torchaudio
+
+from omnivoice.models.omnivoice import OmniVoice
+from omnivoice.utils.common import str2bool
+
+
+def get_best_device():
+ """Auto-detect the best available device: CUDA > MPS > CPU."""
+ if torch.cuda.is_available():
+ return "cuda"
+ if torch.backends.mps.is_available():
+ return "mps"
+ return "cpu"
+
+
+def get_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="OmniVoice single-item inference",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ "--model",
+ type=str,
+ default="k2-fsa/OmniVoice",
+ help="Model checkpoint path or HuggingFace repo id.",
+ )
+ parser.add_argument(
+ "--text",
+ type=str,
+ required=True,
+ help="Text to synthesize.",
+ )
+ parser.add_argument(
+ "--output",
+ type=str,
+ required=True,
+ help="Output WAV file path.",
+ )
+ # Voice cloning
+ parser.add_argument(
+ "--ref_audio",
+ type=str,
+ default=None,
+ help="Reference audio file path for voice cloning.",
+ )
+ parser.add_argument(
+ "--ref_text",
+ type=str,
+ default=None,
+ help="Reference text describing the reference audio.",
+ )
+ # Voice design
+ parser.add_argument(
+ "--instruct",
+ type=str,
+ default=None,
+ help="Style instruction for voice design mode.",
+ )
+ parser.add_argument(
+ "--language",
+ type=str,
+ default=None,
+ help="Language name (e.g. 'English') or code (e.g. 'en').",
+ )
+ # Generation parameters
+ parser.add_argument("--num_step", type=int, default=32)
+ parser.add_argument("--guidance_scale", type=float, default=2.0)
+ parser.add_argument("--speed", type=float, default=1.0)
+ parser.add_argument(
+ "--duration",
+ type=float,
+ default=None,
+ help="Fixed output duration in seconds. If set, overrides the "
+ "model's duration estimation. The speed factor is automatically "
+ "adjusted to match while preserving language-aware pacing.",
+ )
+ parser.add_argument("--t_shift", type=float, default=0.1)
+ parser.add_argument("--denoise", type=str2bool, default=True)
+ parser.add_argument(
+ "--postprocess_output",
+ type=str2bool,
+ default=True,
+ )
+ parser.add_argument("--layer_penalty_factor", type=float, default=5.0)
+ parser.add_argument("--position_temperature", type=float, default=5.0)
+ parser.add_argument("--class_temperature", type=float, default=0.0)
+ parser.add_argument(
+ "--device",
+ type=str,
+ default=None,
+ help="Device to use for inference. Auto-detected if not specified.",
+ )
+ return parser
+
+
+def main():
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ args = get_parser().parse_args()
+
+ device = args.device or get_best_device()
+ logging.info(f"Loading model from {args.model} on {device} ...")
+ model = OmniVoice.from_pretrained(
+ args.model, device_map=device, dtype=torch.float16
+ )
+
+ logging.info(f"Generating audio for: {args.text[:80]}...")
+ audios = model.generate(
+ text=args.text,
+ language=args.language,
+ ref_audio=args.ref_audio,
+ ref_text=args.ref_text,
+ instruct=args.instruct,
+ duration=args.duration,
+ num_step=args.num_step,
+ guidance_scale=args.guidance_scale,
+ speed=args.speed,
+ t_shift=args.t_shift,
+ denoise=args.denoise,
+ postprocess_output=args.postprocess_output,
+ layer_penalty_factor=args.layer_penalty_factor,
+ position_temperature=args.position_temperature,
+ class_temperature=args.class_temperature,
+ )
+
+ torchaudio.save(args.output, audios[0], model.sampling_rate)
+ logging.info(f"Saved to {args.output}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/cli/infer_batch.py b/omnivoice/cli/infer_batch.py
new file mode 100644
index 00000000..82ad1af8
--- /dev/null
+++ b/omnivoice/cli/infer_batch.py
@@ -0,0 +1,527 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Batch inference CLI for OmniVoice.
+
+Distributes TTS generation across multiple GPUs for large-scale tasks.
+Reads a JSONL test list, generates audio in parallel, and saves results.
+
+Usage:
+ omnivoice-infer-batch --model k2-fsa/OmniVoice \
+ --test_list test.jsonl --res_dir results/
+
+Test list format (JSONL, one JSON object per line):
+ Required fields: "id", "text"
+ Voice cloning: "ref_audio", "ref_text"
+ Voice design: "instruct"
+ Optional: "language_id", "language_name", "duration", "speed"
+"""
+
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import signal
+import time
+import traceback
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from typing import List, Optional, Tuple
+
+import torch
+import torchaudio
+from tqdm import tqdm
+
+from omnivoice.models.omnivoice import OmniVoice
+from omnivoice.utils.audio import load_audio
+from omnivoice.utils.common import str2bool
+from omnivoice.utils.data_utils import read_test_list
+from omnivoice.utils.duration import RuleDurationEstimator
+
+
+def get_best_device():
+ """Auto-detect the best available device: CUDA > MPS > CPU."""
+ if torch.cuda.is_available():
+ return "cuda", torch.cuda.device_count()
+ if torch.backends.mps.is_available():
+ return "mps", 1
+ return "cpu", 1
+
+
+worker_model = None
+SAMPLING_RATE = 24000
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(description="Infer OmniVoice Model")
+ parser.add_argument(
+ "--model",
+ type=str,
+ default="k2-fsa/OmniVoice",
+ help="Path to the model checkpoint (local dir or HF repo id). "
+ "Audio tokenizer is expected at /audio_tokenizer/.",
+ )
+ parser.add_argument(
+ "--test_list",
+ type=str,
+ required=True,
+ help="Path to the JSONL file containing test samples. "
+ 'Each line is a JSON object: {"id": "name", "text": "...", '
+ '"ref_audio": "/path.wav", "ref_text": "...", '
+ '"language_id": "en", "language_name": "English", '
+ '"duration": 10.0, "speed": 1.2}. '
+ "language_id, language_name, duration, and speed are optional.",
+ )
+ parser.add_argument(
+ "--res_dir",
+ type=str,
+ required=True,
+ help="Directory to save the generated audio files.",
+ )
+ parser.add_argument(
+ "--num_step",
+ type=int,
+ default=32,
+ help="Number of steps for iterative decoding.",
+ )
+ parser.add_argument(
+ "--guidance_scale",
+ type=float,
+ default=2.0,
+ help="Scale for Classifier-Free Guidance.",
+ )
+ parser.add_argument(
+ "--t_shift",
+ type=float,
+ default=0.1,
+ help="Shift t to smaller ones if t_shift < 1.0",
+ )
+ parser.add_argument(
+ "--nj_per_gpu",
+ type=int,
+ default=1,
+ help="Number of worker processes to spawn per GPU.",
+ )
+ parser.add_argument(
+ "--audio_chunk_duration",
+ type=float,
+ default=15.0,
+ help="Maximum duration of audio chunk (in seconds) for splitting. "
+ '"Not split" if <= 0.',
+ )
+ parser.add_argument(
+ "--audio_chunk_threshold",
+ type=float,
+ default=30.0,
+ help=(
+ "The duration threshold (in seconds) to decide"
+ " whether to split audio into chunks."
+ ),
+ )
+ parser.add_argument(
+ "--batch_duration",
+ type=float,
+ default=1000.0,
+ help="Maximum total duration (reference + generated) per batch (seconds). "
+ "Only effective for parallel_chunk / no chunk mode.",
+ )
+ parser.add_argument(
+ "--batch_size",
+ type=int,
+ default=0,
+ help="Fixed batch size (number of samples per batch). "
+ "If > 0, use fixed-size batching instead of duration-based batching.",
+ )
+ parser.add_argument(
+ "--warmup",
+ type=int,
+ default=0,
+ help="Number of dummy inference runs per worker before real inference "
+ "starts, to warm up CUDA kernels and caches.",
+ )
+ parser.add_argument(
+ "--preprocess_prompt",
+ type=str2bool,
+ default=True,
+ help="Whether to preprocess reference audio (silence removal, trimming). "
+ "Set to False to keep raw audio.",
+ )
+ parser.add_argument(
+ "--postprocess_output",
+ type=str2bool,
+ default=True,
+ help="Whether to post-process generated audio (remove silence).",
+ )
+ parser.add_argument(
+ "--layer_penalty_factor",
+ type=float,
+ default=5.0,
+ help="The penalty factor for layer-wise sampling.",
+ )
+ parser.add_argument(
+ "--position_temperature",
+ type=float,
+ default=5.0,
+ help="The temperature for position selection.",
+ )
+ parser.add_argument(
+ "--class_temperature",
+ type=float,
+ default=0.0,
+ help="The temperature for class token sampling.",
+ )
+ parser.add_argument(
+ "--denoise",
+ type=str2bool,
+ default=True,
+ help="Whether to add <|denoise|> token in the reference.",
+ )
+ parser.add_argument(
+ "--lang_id",
+ type=str,
+ default=None,
+ help="Language id to use when test_list JSONL entries do not contain "
+ "language_id/language_name fields. If provided, both language_id and "
+ "language_name will be set to this value.",
+ )
+ return parser
+
+
+def process_init(rank_queue, model_checkpoint, warmup=0):
+ """Initializer for each worker process.
+
+ Loads model (with tokenizers and duration estimator) onto a specific GPU
+ via ``OmniVoice.from_pretrained()``.
+ """
+ global worker_model
+
+ torch.set_num_threads(2)
+ torch.set_num_interop_threads(2)
+
+ formatter = (
+ "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] "
+ "[Worker %(process)d] %(message)s"
+ )
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ rank = rank_queue.get()
+ device_type, device_id = rank
+ if device_type == "cpu":
+ worker_device = "cpu"
+ elif device_type == "mps":
+ worker_device = "mps"
+ else:
+ worker_device = f"cuda:{device_id}"
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+ worker_model = OmniVoice.from_pretrained(
+ model_checkpoint,
+ device_map=worker_device,
+ dtype=torch.float16,
+ )
+
+ if warmup > 0:
+ logging.info(f"Running {warmup} warmup iterations on {worker_device}")
+ dummy_ref_audio = (
+ torch.randn(1, SAMPLING_RATE),
+ SAMPLING_RATE,
+ ) # 1s silence
+ for i in range(warmup):
+ worker_model.generate(
+ text=["hello"],
+ language=["en"],
+ ref_audio=[dummy_ref_audio],
+ ref_text=["hello"],
+ )
+ logging.info(f"Warmup complete on {worker_device}")
+
+ logging.info(f"Worker on {worker_device} initialized successfully.")
+
+
+def estimate_sample_total_duration(
+ duration_estimator: RuleDurationEstimator,
+ text: str,
+ ref_text: str,
+ ref_audio_path: str,
+ gen_duration: Optional[float] = None,
+) -> float:
+ ref_wav = load_audio(ref_audio_path, SAMPLING_RATE)
+ ref_duration = ref_wav.shape[-1] / SAMPLING_RATE
+
+ if gen_duration is None:
+ gen_duration = duration_estimator.estimate_duration(
+ text, ref_text, ref_duration, low_threshold=2.0
+ )
+
+ total_duration = ref_duration + gen_duration
+ return total_duration
+
+
+def cluster_samples_by_duration(
+ samples: List[Tuple],
+ duration_estimator: RuleDurationEstimator,
+ batch_duration: float,
+) -> List[List[Tuple]]:
+ sample_with_duration = []
+ for sample in samples:
+ save_name, ref_text, ref_audio_path, text, lang_id, lang_name, dur, spd, instruct = sample
+ total_duration = estimate_sample_total_duration(
+ duration_estimator,
+ text,
+ ref_text,
+ ref_audio_path,
+ gen_duration=dur,
+ )
+ sample_with_duration.append((sample, total_duration))
+
+ sample_with_duration.sort(key=lambda x: x[1], reverse=True)
+ batches = []
+ current_batch = []
+ current_total_duration = 0.0
+
+ for sample, duration in sample_with_duration:
+ if duration > batch_duration:
+ batches.append([sample])
+ continue
+
+ if current_total_duration + duration <= batch_duration:
+ current_batch.append(sample)
+ current_total_duration += duration
+ else:
+ batches.append(current_batch)
+ current_batch = [sample]
+ current_total_duration = duration
+
+ if current_batch:
+ batches.append(current_batch)
+
+ logging.info(f"Clustered {len(samples)} samples into {len(batches)} batches")
+ return batches
+
+
+def cluster_samples_by_batch_size(
+ samples: List[Tuple],
+ duration_estimator: RuleDurationEstimator,
+ batch_size: int,
+) -> List[List[Tuple]]:
+ """Split samples into fixed-size batches, sorted by duration to minimize padding."""
+ sample_with_duration = []
+ for sample in samples:
+ save_name, ref_text, ref_audio_path, text, lang_id, lang_name, dur, spd, instruct = sample
+ total_duration = estimate_sample_total_duration(
+ duration_estimator,
+ text,
+ ref_text,
+ ref_audio_path,
+ gen_duration=dur,
+ )
+ sample_with_duration.append((sample, total_duration))
+
+ sample_with_duration.sort(key=lambda x: x[1], reverse=True)
+ sorted_samples = [s for s, _ in sample_with_duration]
+
+ batches = [
+ sorted_samples[i : i + batch_size]
+ for i in range(0, len(sorted_samples), batch_size)
+ ]
+ logging.info(
+ f"Split {len(samples)} samples into {len(batches)} batches "
+ f"(fixed batch_size={batch_size}, sorted by duration)"
+ )
+ return batches
+
+
+def run_inference_batch(
+ batch_samples: List[Tuple],
+ res_dir: str,
+ **gen_kwargs,
+) -> List[Tuple]:
+ global worker_model
+
+ save_names = []
+ ref_texts = []
+ ref_audio_paths = []
+ texts = []
+ langs = []
+ durations = []
+ speeds = []
+ instructs = []
+
+ for sample in batch_samples:
+ save_name, ref_text, ref_audio_path, text, lang_id, lang_name, dur, spd, instruct = sample
+ save_names.append(save_name)
+ ref_texts.append(ref_text)
+ ref_audio_paths.append(ref_audio_path)
+ texts.append(text)
+ langs.append(lang_id)
+ durations.append(dur)
+ speeds.append(spd)
+ instructs.append(instruct)
+
+ start_time = time.time()
+ audios = worker_model.generate(
+ text=texts,
+ language=langs,
+ ref_audio=ref_audio_paths,
+ ref_text=ref_texts,
+ duration=durations if any(d is not None for d in durations) else None,
+ speed=speeds if any(s is not None for s in speeds) else None,
+ instruct=instructs if any(i is not None for i in instructs) else None,
+ **gen_kwargs,
+ )
+ batch_synth_time = time.time() - start_time
+
+ results = []
+ for save_name, audio in zip(save_names, audios):
+ save_path = os.path.join(res_dir, save_name + ".wav")
+ torchaudio.save(save_path, audio, worker_model.sampling_rate)
+ audio_duration = audio.shape[-1] / worker_model.sampling_rate
+ results.append(
+ (
+ save_name,
+ batch_synth_time / len(batch_samples),
+ audio_duration,
+ "success",
+ )
+ )
+
+ return results
+
+
+def main():
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+ mp.set_start_method("spawn", force=True)
+
+ args = get_parser().parse_args()
+ os.makedirs(args.res_dir, exist_ok=True)
+
+ device_type, num_devices = get_best_device()
+ if device_type == "cpu":
+ logging.warning(
+ "No GPU found. Falling back to CPU inference. This might be slow."
+ )
+
+ num_processes = num_devices * args.nj_per_gpu
+ logging.info(
+ f"Using {device_type} ({num_devices} device(s))."
+ f" Spawning {num_processes} worker processes."
+ )
+
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+ for rank in list(range(num_devices)) * args.nj_per_gpu:
+ rank_queue.put((device_type, rank))
+
+ samples_raw = read_test_list(args.test_list)
+ samples = []
+ for s in samples_raw:
+ if args.lang_id is not None:
+ lang_id = args.lang_id
+ lang_name = args.lang_id
+ else:
+ lang_id = s.get("language_id")
+ lang_name = s.get("language_name")
+ samples.append(
+ (
+ s["id"],
+ s["ref_text"],
+ s["ref_audio"],
+ s["text"],
+ lang_id,
+ lang_name,
+ s.get("duration"),
+ s.get("speed"),
+ s.get("instruct"),
+ )
+ )
+
+ total_synthesis_time = []
+ total_audio_duration = []
+
+ try:
+ with ProcessPoolExecutor(
+ max_workers=num_processes,
+ initializer=process_init,
+ initargs=(rank_queue, args.model, args.warmup),
+ ) as executor:
+ futures = []
+
+ # parallel_chunk / no chunk
+ logging.info("Running batch inference")
+
+ duration_estimator = RuleDurationEstimator()
+ if args.batch_size > 0:
+ batches = cluster_samples_by_batch_size(
+ samples, duration_estimator, args.batch_size
+ )
+ else:
+ batches = cluster_samples_by_duration(
+ samples, duration_estimator, args.batch_duration
+ )
+
+ args_dict = vars(args)
+
+ for batch in batches:
+ futures.append(
+ executor.submit(
+ run_inference_batch, batch_samples=batch, **args_dict
+ )
+ )
+
+ for future in tqdm(
+ as_completed(futures), total=len(futures), desc="Processing samples"
+ ):
+ try:
+ result = future.result()
+ for s_name, synth_time, audio_dur, status in result:
+ total_synthesis_time.append(synth_time)
+ total_audio_duration.append(audio_dur)
+ rtf = synth_time / audio_dur if audio_dur > 0 else float("inf")
+ logging.debug(
+ f"Processed {s_name}: Audio Duration={audio_dur:.2f}s, "
+ f"Synthesis Time={synth_time:.2f}s, RTF={rtf:.4f}"
+ )
+ except Exception as e:
+ logging.error(f"Failed to process sample: {e}")
+ detailed_error = traceback.format_exc()
+ logging.error(f"Detailed error: {detailed_error}")
+
+ except (Exception, KeyboardInterrupt) as e:
+ logging.critical(
+ f"An unrecoverable error occurred: {e}. Terminating all processes."
+ )
+ detailed_error_info = traceback.format_exc()
+ logging.error(f"--- DETAILED TRACEBACK ---\n{detailed_error_info}")
+ os.killpg(os.getpgid(os.getpid()), signal.SIGKILL)
+
+ total_synthesis_time = sum(total_synthesis_time)
+ total_audio_duration = sum(total_audio_duration)
+ logging.info("--- Summary ---")
+ logging.info(f"Total audio duration: {total_audio_duration:.2f}s")
+ logging.info(f"Total synthesis time: {total_synthesis_time:.2f}s")
+ if total_audio_duration > 0:
+ average_rtf = total_synthesis_time / total_audio_duration
+ logging.info(f"Average RTF: {average_rtf:.4f}")
+ else:
+ logging.warning("No speech was generated. RTF cannot be computed.")
+
+ logging.info("Done!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/cli/train.py b/omnivoice/cli/train.py
new file mode 100644
index 00000000..947746e1
--- /dev/null
+++ b/omnivoice/cli/train.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Training CLI for OmniVoice.
+
+Launches distributed training via HuggingFace Accelerate.
+Supports pre-training on Emilia data and finetuning on custom data.
+
+Usage:
+ accelerate launch --gpu_ids 0,1,2,3 --num_processes 4 \\
+ -m omnivoice.cli.train \\
+ --train_config train_config.json \\
+ --data_config data_config.json \\
+ --output_dir output/
+
+See examples/run_emilia.sh and examples/run_finetune.sh for full pipelines.
+"""
+
+import argparse
+
+from omnivoice.training.builder import build_dataloaders, build_model_and_tokenizer
+from omnivoice.training.config import TrainingConfig
+from omnivoice.training.trainer import OmniTrainer
+
+
+def main():
+ parser = argparse.ArgumentParser(description="OmniVoice Training Entry Point")
+ parser.add_argument(
+ "--train_config", type=str, required=True, help="Path to config JSON"
+ )
+ parser.add_argument(
+ "--output_dir", type=str, required=True, help="Where to save checkpoints"
+ )
+ parser.add_argument(
+ "--data_config", type=str, required=True, help="Path to data config JSON"
+ )
+ args = parser.parse_args()
+
+ # 1. Load Configuration
+ config = TrainingConfig.from_json(args.train_config)
+ config.output_dir = args.output_dir
+ config.data_config = args.data_config
+
+ # 2. Build Components
+ model, tokenizer = build_model_and_tokenizer(config)
+ train_loader, eval_loader = build_dataloaders(config, tokenizer)
+
+ # 3. Initialize Trainer and Start
+ trainer = OmniTrainer(
+ model=model,
+ config=config,
+ train_dataloader=train_loader,
+ eval_dataloader=eval_loader,
+ tokenizer=tokenizer,
+ )
+ trainer.train()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/data/__init__.py b/omnivoice/data/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/data/batching.py b/omnivoice/data/batching.py
new file mode 100644
index 00000000..40dddb9c
--- /dev/null
+++ b/omnivoice/data/batching.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Batching strategies for streaming/iterable datasets.
+
+Provides length-based grouping and packing for efficient training with
+variable-length audio.
+
+Key classes:
+- ``PackingIterableDataset``: Packs multiple samples into fixed-length sequences
+ for training. Used by ``omnivoice.training.builder``.
+- ``StreamLengthGroupDataset``: Groups samples by length into buckets. Used by
+ data processing scripts (e.g. ``omnivoice/scripts/``).
+"""
+
+import bisect
+import logging
+from typing import Any, Dict, Iterator, List, Optional
+
+import numpy as np
+
+from omnivoice.data.dataset import IterableDataReader, WrappedIterableDataset
+
+
+class StreamLengthGroupDataset(WrappedIterableDataset):
+ """A streaming dataset that groups samples by their lengths into buckets.
+ Only support audio data for now."""
+
+ def __init__(
+ self,
+ dataset: IterableDataReader,
+ batch_duration: float,
+ min_length: float = 0.5,
+ max_length: float = 30.0,
+ num_buckets: int = 20,
+ audio_key: str = "audio",
+ drop_last: bool = False,
+ max_sample: Optional[int] = None,
+ ):
+ self.dataset = dataset
+ self.batch_duration = batch_duration
+ self.min_length = min_length
+ self.max_length = max_length
+ self.num_buckets = num_buckets
+ self.audio_key = audio_key
+ self.drop_last = drop_last
+ self.max_sample = max_sample if max_sample is not None else float("inf")
+
+ self.boundaries = np.linspace(min_length, max_length, num_buckets + 1)[1:]
+
+ def set_epoch(self, epoch: int):
+ """
+ Set the epoch for shuffling.
+ """
+ self.dataset.set_epoch(epoch)
+
+ def _get_bucket_id(self, length: float) -> int:
+
+ return bisect.bisect_left(self.boundaries, length)
+
+ def __iter__(self) -> Iterator[List[Dict[str, Any]]]:
+ buckets = [[] for _ in range(self.num_buckets)]
+ bucket_max_len = [0.0] * self.num_buckets
+
+ for sample in self.dataset:
+ audio = sample[self.audio_key]
+ duration = audio.size(-1) / self.dataset.sample_rate
+
+ if duration < self.min_length or duration > self.max_length:
+ # logging.warning(f"Skipping sample with duration {duration:.2f}s")
+ continue
+
+ b_id = self._get_bucket_id(duration)
+ buckets[b_id].append(sample)
+
+ if duration > bucket_max_len[b_id]:
+ bucket_max_len[b_id] = duration
+
+ if (
+ bucket_max_len[b_id] * (len(buckets[b_id]) + 1) >= self.batch_duration
+ or len(buckets[b_id]) >= self.max_sample
+ ):
+ yield buckets[b_id]
+ buckets[b_id] = []
+ bucket_max_len[b_id] = 0.0
+
+ if not self.drop_last:
+ for b_idx, bucket in enumerate(buckets):
+ if bucket:
+ yield bucket
+ buckets[b_idx] = []
+
+
+class PackingIterableDataset(WrappedIterableDataset):
+ """
+ An IterableDataset that dynamically processes samples using a processor
+ and packs them into batches based on the real token count.
+
+ Args:
+ dataset (Iterable): The raw dataset to process.
+ processor (Callable): A processor to process each sample.
+ batch_tokens (int): Maximum number of tokens per batch.
+ """
+
+ def __init__(
+ self,
+ dataset: IterableDataReader,
+ processor: Any,
+ batch_tokens: int,
+ ):
+ self.dataset = dataset
+ self.processor = processor
+ self.batch_tokens = batch_tokens
+ self.skip_batches = 0
+
+ def set_epoch(self, epoch: int):
+ """
+ Set the epoch for shuffling.
+ """
+ self.dataset.set_epoch(epoch)
+
+ def __iter__(self) -> Iterator[List[Dict[str, Any]]]:
+ current_batch = []
+ current_token_count = 0
+
+ for raw_sample in self.dataset:
+ # Process the sample using the processor
+ try:
+ processed_sample = self.processor(raw_sample)
+ except Exception as e:
+ logging.warning(f"Error processing sample {raw_sample}: {e}")
+ continue
+
+ sample_length = processed_sample["length"]
+
+ if sample_length > self.batch_tokens:
+ continue
+
+ # Check if adding this sample exceeds the batch token limit
+ if current_token_count + sample_length > self.batch_tokens:
+ # Yield the current batch and start a new one
+ yield current_batch
+ current_batch = []
+ current_token_count = 0
+
+ # Add the processed sample to the current batch
+ current_batch.append(processed_sample)
+ current_token_count += sample_length
+
+ # Yield the last batch if it's not empty
+ if current_batch:
+ yield current_batch
diff --git a/omnivoice/data/collator.py b/omnivoice/data/collator.py
new file mode 100644
index 00000000..8ce55839
--- /dev/null
+++ b/omnivoice/data/collator.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Data collator with packing for efficient training.
+
+Packs multiple samples into a single sequence of fixed length (``batch_tokens``)
+to maximize GPU utilization, instead of padding each sample individually.
+Used by ``omnivoice.training.builder`` to create the collate function.
+"""
+
+from typing import Any, Dict, List
+
+import torch
+
+
+class PackingDataCollator:
+ def __init__(self, processor, batch_tokens: int):
+ self.batch_tokens = batch_tokens
+ self.processor = processor
+
+ def __call__(self, processed_samples: List[Dict[str, Any]]) -> Dict[str, Any]:
+
+ target_length = self.batch_tokens
+
+ input_ids = torch.cat(
+ [s["input_ids"] for s in processed_samples], dim=1
+ ) # [C, Total_Len], C is the number of codebook layers of the audio tokenizer
+ labels = torch.cat(
+ [s["labels"] for s in processed_samples], dim=1
+ ) # [C, Total_Len]
+ audio_mask = torch.cat(
+ [s["audio_mask"] for s in processed_samples], dim=0
+ ) # [Total_Len]
+
+ position_ids = torch.cat(
+ [torch.arange(s["length"], dtype=torch.long) for s in processed_samples],
+ dim=0,
+ ) # [Total_Len]
+
+ pad_length = target_length - input_ids.shape[1]
+
+ input_ids = torch.nn.functional.pad(
+ input_ids,
+ pad=(0, pad_length),
+ value=self.processor.text_tokenizer.pad_token_id,
+ )
+
+ labels = torch.nn.functional.pad(labels, pad=(0, pad_length), value=-100)
+
+ audio_mask = torch.nn.functional.pad(
+ audio_mask, pad=(0, pad_length), value=False
+ )
+
+ position_ids = torch.nn.functional.pad(
+ position_ids, pad=(0, pad_length), value=0
+ )
+
+ return_list = {
+ "input_ids": input_ids.unsqueeze(0), # [1, C, L]
+ "labels": labels.unsqueeze(0), # [1, C, L]
+ "audio_mask": audio_mask.unsqueeze(0), # [1, L]
+ "position_ids": position_ids.unsqueeze(0), # [1, L]
+ }
+
+ document_ids_list = []
+
+ for i, s in enumerate(processed_samples):
+ seq_len = s["length"]
+ document_ids_list.append(torch.full((seq_len,), i, dtype=torch.int32))
+
+ document_ids = torch.cat(document_ids_list, dim=0)
+
+ document_ids = torch.nn.functional.pad(
+ document_ids, pad=(0, pad_length), value=-1
+ )
+ return_list["document_ids"] = document_ids.unsqueeze(0) # [1, L]
+
+ return return_list
diff --git a/omnivoice/data/dataset.py b/omnivoice/data/dataset.py
new file mode 100644
index 00000000..e96a56f8
--- /dev/null
+++ b/omnivoice/data/dataset.py
@@ -0,0 +1,551 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Dataset and data-loading utilities for training and evaluation.
+
+Provides WebDataset-based iterable datasets, manifest parsing, and audio/token
+loading. Used by ``omnivoice.training.builder.build_dataloaders()`` to construct
+train and eval data loaders.
+
+Key functions:
+- ``prepare_data_manifests_from_json()``: Parses a data config JSON into train/dev
+ manifests.
+
+Key classes:
+- ``WebDatasetReader``: Reads audio/text pairs from WebDataset tar shards as an
+ iterable dataset.
+- ``MuxWebDatasetReader``: Multiplexes multiple WebDataset readers for
+ multilingual data.
+- ``JsonlDatasetReader``: Reads audio/text pairs from a JSONL manifest file.
+ Used by data processing scripts (e.g. ``omnivoice/scripts/``).
+- ``SampleDecoder``: Decodes individual samples (audio or tokens + labels).
+"""
+
+import io
+import json
+import logging
+import os
+import random
+from typing import Any, Dict, Iterator, List, Optional, Tuple
+
+import torch
+import torch.distributed as dist
+import torchaudio
+import webdataset as wds
+from torch.utils.data import IterableDataset
+
+
+def load_audio_webdataset(data, sample_rate: int = 24000, device="cpu"):
+ """
+ Load audio from bytes data and resample to the target sample rate if needed.
+ Return a tensor of shape (1, num_samples)
+ """
+ audio, sr = torchaudio.load(io.BytesIO(data))
+ audio = audio.to(device)
+ if audio.size(dim=0) > 1:
+ audio = torch.mean(audio, dim=0)
+ if sr != sample_rate:
+ audio = torchaudio.functional.resample(audio, sr, sample_rate)
+ return audio
+
+
+def prepare_data_manifests_from_json(
+ data_config: str,
+) -> Tuple[List[Tuple[str, str, int, float]], List[Tuple[str, str, int, float]]]:
+ """
+ Prepare data manifests from a json file.
+ A typical multilingual json file is in the following format:
+ {
+ "train":
+ [
+ {
+ "language_id": "en",
+ "manifest_path": [
+ "/Emilia/EN/data.lst"
+ ],
+ "repeat": 1
+ },
+ {
+ "language_id": "zh",
+ "manifest_path": [
+ "/Emilia/ZH/data.lst"
+ ],
+ "repeat": 1
+ }
+ ],
+ "dev":
+ [
+ {
+ "language_id": "en",
+ "manifest_path": [
+ "/Emilia/EN-dev/data.lst"
+ ],
+ "repeat": 1
+ },
+ {
+ "language_id": "zh",
+ "manifest_path": [
+ "/Emilia/ZH-dev/data.lst"
+ ],
+ "repeat": 1
+ }
+ ]
+ }
+
+ "language_id" is not used, just for better organization of multilingual data.
+ "repeat" is an optional field, default to 1, which indicates how many times
+ the manifest should be repeated.
+
+ The simplist format is like:
+ {
+ "train":
+ [
+ {
+ "manifest_path": [
+ "/Emilia/EN/data.lst",
+ "/Emilia/ZH/data.lst"
+ ],
+ }
+ ],
+ "dev":
+ [
+ {
+ "manifest_path": [
+ "/Emilia/EN-dev/data.lst",
+ "/Emilia/ZH-dev/data.lst"
+ ],
+ }
+ ]
+
+ data.lst format (items separated by space):
+ /path/to/data.tar /path/to/label.jsonl num_items num_seconds
+ """
+ train_manifests = []
+ dev_manifests = []
+ with open(data_config, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ for item in data["train"]:
+ manifest_paths = item["manifest_path"]
+ repeat = item.get("repeat", 1)
+ for manifest_path in manifest_paths:
+ # assert manifest_path is a file
+ assert os.path.isfile(manifest_path), f"{manifest_path} is not a file."
+ train_manifests.extend(
+ webdataset_manifest_reader(manifest_path) * repeat
+ )
+ if "dev" in data:
+ for item in data["dev"]:
+ manifest_paths = item["manifest_path"]
+ repeat = item.get("repeat", 1)
+ for manifest_path in manifest_paths:
+ dev_manifests.extend(
+ webdataset_manifest_reader(manifest_path) * repeat
+ )
+ return train_manifests, dev_manifests
+
+
+def webdataset_manifest_reader(
+ manifest_path: str,
+) -> List[Tuple[str, str]]:
+ """
+ Read a manifest file containing webdataset tar paths and label jsonl paths.
+ Each line in the manifest file is in the format of:
+ /path/to/data.tar /path/to/label.jsonl num_items num_seconds
+ """
+ manifests = []
+ with open(manifest_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ parts = line.split()
+ if len(parts) != 4:
+ raise ValueError(
+ f"Invalid manifest line: {line}. "
+ f"Each line must contain "
+ "tar_path, label_jsonl_path, num_items, num_seconds."
+ )
+ tar_path, label_jsonl_path, num_items, num_seconds = (
+ parts[0],
+ parts[1],
+ int(parts[2]),
+ float(parts[3]),
+ )
+ manifests.append((tar_path, label_jsonl_path, num_items, num_seconds))
+ return manifests
+
+
+class SampleDecoder:
+ """
+ Decode a sample from webdataset, including loading audio/tokens and fetching label.
+ """
+
+ def __init__(
+ self,
+ tar_to_label: Dict,
+ sample_rate: int = 24000,
+ audio_format: Optional[Tuple[str]] = None,
+ normalize_audio: bool = True,
+ ):
+ """
+ Args:
+ tar_to_label:
+ A dict mapping from audio tar file to label tar file.
+ sample_rate:
+ Target sample rate for audio. Required if audio is loaded.
+ audio_format:
+ Tuple of audio file extensions to look for in the sample.
+ """
+ self.tar_to_label = tar_to_label
+ self.sample_rate = sample_rate
+ self.label_dataset = None
+ if audio_format is None:
+ self.audio_format = ("flac", "wav", "mp3")
+ else:
+ self.audio_format = audio_format
+ self.normalize_audio = normalize_audio
+
+ def __call__(self, sample):
+ return_dict = {}
+ src = sample["__url__"]
+ key = sample["__key__"]
+ if (
+ self.label_dataset is None
+ or self.label_dataset.path != self.tar_to_label[src]
+ ):
+ self.label_dataset = LabelDataset(self.tar_to_label[src])
+
+ audio = torch.empty(0)
+ if "npy" in sample:
+ audio_tokens = torch.from_numpy(sample["npy"])
+ return_dict["audio_tokens"] = audio_tokens
+ else:
+ for ext in self.audio_format:
+ if ext in sample:
+ # load audio (1, num_samples)
+ audio = load_audio_webdataset(
+ sample[ext], sample_rate=self.sample_rate
+ )
+ if self.normalize_audio:
+ audio = (audio / (audio.abs().max() + 1e-7)) * 0.9
+ break
+ return_dict["audio"] = audio
+ return_dict["audio_duration"] = audio.size(-1) / self.sample_rate
+
+ label = self.label_dataset[key]
+
+ return_dict["label"] = label
+ return return_dict
+
+
+class LabelDataset:
+ def __init__(self, jsonl_path: str):
+ """
+ Load labels from a jsonl file.
+ Args:
+ jsonl_path:
+ Path to the jsonl file containing labels.
+ Each line in the manifest file is in the format of:
+ {"idx": "idx", "text": "transcription text"}
+ """
+ self._labels = {}
+ self.path = jsonl_path
+ if not os.path.exists(jsonl_path):
+ raise FileNotFoundError(f"Label jsonl file {jsonl_path} does not exist.")
+ with open(jsonl_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ item = json.loads(line)
+ if "id" in item:
+ self._labels[item["id"]] = item
+
+ def __getitem__(self, key):
+ return self._labels[key]
+
+
+class IterableDataReader:
+ "Interfaces for classes reading data."
+
+ sample_rate: int
+
+ def set_epoch(self, epoch: int):
+ raise NotImplementedError
+
+ def __iter__(self) -> Iterator[Dict[str, Any]]:
+ raise NotImplementedError
+
+ def __len__(self) -> int:
+ raise NotImplementedError
+
+
+class WrappedIterableDataset(IterableDataset):
+ "IterableDataset interfaces in this project."
+
+ def set_epoch(self, epoch: int):
+ raise NotImplementedError
+
+ def __iter__(self) -> Iterator[List[Dict[str, Any]]]:
+ raise NotImplementedError
+
+
+class WebDatasetReader(IterableDataReader):
+ def __init__(
+ self,
+ manifests: List[Tuple[str, str, int, float]],
+ evaluation: bool = False,
+ shuffle_buffer_size: int = 20000,
+ sample_rate: int = 24000,
+ ):
+ self.shuffle_buffer_size = shuffle_buffer_size
+ self.evaluation = evaluation
+ self.epoch = 0
+
+ self.orig_urls = []
+ self.tar_to_label = {}
+ self.num_items = 0
+ self.num_seconds = 0.0
+ for tar_path, label_jsonl_path, num_items, num_seconds in manifests:
+ self.orig_urls.append(tar_path)
+ self.tar_to_label[tar_path] = label_jsonl_path
+ self.num_items += num_items
+ self.num_seconds += num_seconds
+ self.urls = self.orig_urls.copy()
+ self.sample_decoder = SampleDecoder(
+ tar_to_label=self.tar_to_label,
+ sample_rate=sample_rate,
+ )
+ self.sample_rate = sample_rate
+
+ def set_epoch(self, epoch: int):
+ """
+ Set the epoch for shuffling.
+ """
+ self.epoch = epoch
+ self.urls = self.orig_urls.copy()
+ if not self.evaluation:
+ random.Random(epoch).shuffle(self.urls)
+
+ def __iter__(self) -> Iterator[Dict[str, Any]]:
+
+ dataset = wds.WebDataset(
+ self.urls,
+ shardshuffle=False,
+ workersplitter=wds.split_by_worker,
+ nodesplitter=wds.split_by_node,
+ )
+
+ pipeline = dataset.decode().map(self.sample_decoder)
+ if not self.evaluation:
+ pipeline = pipeline.shuffle(self.shuffle_buffer_size, seed=self.epoch)
+ return iter(pipeline)
+
+ def __len__(self) -> int:
+ return self.num_items
+
+
+class JsonlDatasetReader(IterableDataReader):
+ """Read raw JSONL and load audio files, matching WebDatasetReader output format.
+
+ Each JSONL line should be a JSON object with at least:
+ {"id": "...", "audio_path": "/path/to/audio.wav", ...}
+
+ Yields dicts of the form: {"audio": Tensor(1, T), "label": dict}
+ """
+
+ def __init__(
+ self,
+ jsonl_path: str,
+ sample_rate: int = 24_000,
+ shuffle: bool = True,
+ shuffle_seed: int = 42,
+ normalize_audio: bool = True,
+ ):
+ self.jsonl_path = jsonl_path
+ self.sample_rate = sample_rate
+ self.shuffle = shuffle
+ self.shuffle_seed = shuffle_seed
+ self.normalize_audio = normalize_audio
+
+ def set_epoch(self, epoch: int):
+ self.shuffle_seed = epoch
+
+ def _read_lines(self) -> list[dict]:
+ entries = []
+ with open(self.jsonl_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ entries.append(json.loads(line))
+ if self.shuffle:
+ random.seed(self.shuffle_seed)
+ random.shuffle(entries)
+ logging.info(
+ f"Shuffled {len(entries)} JSONL entries (seed={self.shuffle_seed})"
+ )
+ return entries
+
+ def _stream_lines(self):
+ with open(self.jsonl_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ yield json.loads(line)
+
+ def __iter__(self):
+ source = self._read_lines() if self.shuffle else self._stream_lines()
+
+ # Split data across distributed ranks (multi-GPU / DDP)
+ if dist.is_initialized():
+ rank = dist.get_rank()
+ world_size = dist.get_world_size()
+ source = [item for i, item in enumerate(source) if i % world_size == rank]
+
+ # Split data across DataLoader workers to avoid duplication
+ worker_info = torch.utils.data.get_worker_info()
+ if worker_info is not None:
+ source = (
+ item
+ for i, item in enumerate(source)
+ if i % worker_info.num_workers == worker_info.id
+ )
+
+ for meta in source:
+ audio_path = meta.get("audio_path")
+ if not audio_path or not os.path.exists(audio_path):
+ logging.warning(
+ f"Skipping {meta.get('id', '?')}: audio_path missing or not found"
+ )
+ continue
+ try:
+ waveform, sr = torchaudio.load(audio_path)
+ if waveform.shape[0] > 1:
+ waveform = waveform.mean(dim=0, keepdim=True)
+ if sr != self.sample_rate:
+ waveform = torchaudio.functional.resample(
+ waveform, sr, self.sample_rate
+ )
+ if self.normalize_audio:
+ waveform = (waveform / (waveform.abs().max() + 1e-7)) * 0.9
+ meta["audio_duration"] = waveform.shape[1] / self.sample_rate
+ yield {"audio": waveform, "label": meta}
+ except Exception as e:
+ logging.warning(f"Skipping {meta.get('id', '?')}: {e}")
+
+
+class MuxWebDatasetReader(IterableDataReader):
+ def __init__(
+ self,
+ readers: List[WebDatasetReader],
+ weights: Optional[List[float]] = None,
+ stop_early: bool = False,
+ seed: int = 0,
+ ):
+ self.readers = readers
+ self.stop_early = stop_early
+ self.mux_iterator = LazyIteratorMultiplexer(
+ *readers,
+ stop_early=stop_early,
+ weights=weights,
+ seed=seed,
+ )
+
+ def set_epoch(self, epoch: int):
+ """
+ Set the epoch for shuffling.
+ """
+ for reader in self.readers:
+ reader.set_epoch(epoch)
+
+ def __iter__(self) -> Iterator[Dict[str, Any]]:
+ return iter(self.mux_iterator)
+
+
+class LazyIteratorMultiplexer:
+ """
+ A wrapper over multiple iterators that enables to combine
+ lazy manifests in Lhotse. During iteration, unlike
+ :class:`.LazyIteratorChain`,
+ :class:`.LazyIteratorMultiplexer` at each step randomly
+ selects the iterable used to yield an item.
+
+ Since the iterables might be of different length, we provide
+ a ``weights`` parameter to let the user decide which iterables
+ should be sampled more frequently than others.
+ When an iterable is exhausted, we will keep sampling from the other iterables, until
+ we exhaust them all, unless ``stop_early`` is set to ``True``.
+ """
+
+ def __init__(
+ self,
+ *iterators: IterableDataReader,
+ stop_early: bool = False,
+ weights: Optional[List[float]] = None,
+ seed: int = 0,
+ ) -> None:
+ self.iterators = list(iterators)
+ self.stop_early = stop_early
+ self.seed = seed
+
+ assert (
+ len(self.iterators) > 1
+ ), "There have to be at least two iterables to multiplex."
+
+ if weights is None:
+ if all(hasattr(it, "__len__") for it in self.iterators):
+ lengths = [len(it) for it in self.iterators]
+ total_length = sum(lengths)
+ self.weights = [length / total_length for length in lengths]
+ else:
+ self.weights = [1] * len(self.iterators)
+ else:
+ self.weights = weights
+
+ assert len(self.iterators) == len(self.weights)
+
+ def __iter__(self):
+
+ rng = random.Random(self.seed)
+ iters = [iter(it) for it in self.iterators]
+ exhausted = [False for _ in range(len(iters))]
+
+ def should_continue():
+ if self.stop_early:
+ return not any(exhausted)
+ else:
+ return not all(exhausted)
+
+ while should_continue():
+ active_indexes, active_weights = zip(
+ *[
+ (i, w)
+ for i, (is_exhausted, w) in enumerate(zip(exhausted, self.weights))
+ if not is_exhausted
+ ]
+ )
+ idx = rng.choices(active_indexes, weights=active_weights, k=1)[0]
+ selected = iters[idx]
+ try:
+ item = next(selected)
+ yield item
+ except StopIteration:
+ exhausted[idx] = True
+ continue
+
+ def __len__(self) -> int:
+ return sum(len(iterator) for iterator in self.iterators)
diff --git a/omnivoice/data/processor.py b/omnivoice/data/processor.py
new file mode 100644
index 00000000..7e3ec1b3
--- /dev/null
+++ b/omnivoice/data/processor.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Training sample processor for OmniVoice.
+
+Converts raw audio/text samples into model-ready tensors: applies prompt/mask
+tokenization, randomly drops conditioning, and injects language/instruct tokens.
+Used by ``omnivoice.training.builder`` to build the data pipeline.
+
+Contains two processor classes:
+- ``OmniVoiceSampleProcessor``: Full processor used for training.
+- ``OmniVoiceSimpleSampleProcessor``: Simplified processor (not used for training).
+"""
+
+import random
+from typing import Any, Dict
+
+import torch
+
+
+class OmniVoiceSampleProcessor:
+ """
+ Handles the logic of processing a raw sample into tensors
+ (masking, tokenization, etc.).
+ """
+
+ def __init__(
+ self,
+ text_tokenizer: Any,
+ num_channels: int,
+ audio_mask_id: int,
+ prompt_ratio_range: tuple,
+ mask_ratio_range: tuple,
+ drop_cond_ratio: float,
+ language_ratio: float,
+ use_pinyin_ratio: float,
+ instruct_ratio: float,
+ only_instruct_ratio: float,
+ ):
+ self.text_tokenizer = text_tokenizer
+ self.num_channels = num_channels
+ self.audio_mask_id = audio_mask_id
+ self.prompt_ratio_range = prompt_ratio_range
+ self.mask_ratio_range = mask_ratio_range
+ self.drop_cond_ratio = drop_cond_ratio
+
+ self.language_ratio = language_ratio
+ self.use_pinyin_ratio = use_pinyin_ratio
+ self.instruct_ratio = instruct_ratio
+ self.only_instruct_ratio = only_instruct_ratio
+
+ def __call__(self, sample: Dict[str, Any]) -> Dict[str, Any]:
+
+ # clean_start_token_idx is only used for prompt denoising training,
+ # where the prompt region is augmented with noises and the model
+ # needs to learn to recover the clean prompt.
+ # clean_start_token_idx indicates the start index of the clean generated token.
+ if "clean_start_token_idx" in sample["label"]:
+ drop_cond = False
+ else:
+ drop_cond = random.uniform(0, 1) < self.drop_cond_ratio
+
+ if drop_cond:
+ prompt_ratio = 0.0
+ drop_text = True
+ use_language = False
+ use_instruct = False
+ else:
+ prompt_ratio = random.uniform(*self.prompt_ratio_range)
+ drop_text = False
+ use_language = random.uniform(0, 1) < self.language_ratio
+ use_instruct = random.uniform(0, 1) < self.instruct_ratio
+ if use_instruct and random.uniform(0, 1) < self.only_instruct_ratio:
+ prompt_ratio = 0.0
+
+ mask_ratio = random.uniform(*self.mask_ratio_range)
+
+ # --- Style ---
+ style = ""
+ if use_language:
+ language = sample["label"].get("language_id", "None")
+ else:
+ language = "None"
+ if use_instruct:
+ instruct = sample["label"].get("instruct", "None")
+ else:
+ instruct = "None"
+
+ if "clean_start_token_idx" in sample["label"]:
+ style += "<|denoise|>"
+
+ style += f"<|lang_start|>{language}<|lang_end|>"
+ style += f"<|instruct_start|>{instruct}<|instruct_end|>"
+
+ style_inputs = self.text_tokenizer(style, return_tensors="pt").input_ids.repeat(
+ self.num_channels, 1
+ )
+ style_labels = torch.full(
+ style_inputs.shape, -100
+ ) # Style prompt does not compute loss
+
+ # --- Text ---
+ if (
+ "text_pinyin" in sample["label"]
+ and random.uniform(0, 1) < self.use_pinyin_ratio
+ ):
+ text = sample["label"]["text_pinyin"]
+ else:
+ text = sample["label"]["text"]
+ text_inputs = self.text_tokenizer(
+ f"<|text_start|>{text}<|text_end|>", return_tensors="pt"
+ ).input_ids.repeat(self.num_channels, 1)
+ text_labels = torch.full(text_inputs.shape, -100) # Text does not compute loss
+
+ # --- Audio ---
+ audio_tokens = sample["audio_tokens"].long()
+
+ # Masking Logic
+ if "clean_start_token_idx" in sample["label"]:
+ prompt_length = sample["label"]["clean_start_token_idx"]
+ else:
+ prompt_length = int(audio_tokens.shape[1] * prompt_ratio)
+
+ audio_inputs = audio_tokens.clone()
+ audio_labels = audio_tokens.clone()
+
+ # Apply masking
+ maskable_region = audio_tokens[:, prompt_length:]
+ token_mask = torch.rand(maskable_region.shape) < mask_ratio
+ audio_inputs[:, prompt_length:][token_mask] = self.audio_mask_id
+ audio_labels[:, prompt_length:][
+ ~token_mask
+ ] = -100 # Only compute loss on masked tokens
+ if not drop_cond:
+ audio_labels[:, :prompt_length] = -100 # No loss on prompt region
+
+ # --- Concatenation ---
+ if drop_text:
+ input_ids = audio_inputs
+ labels = audio_labels
+ total_length = input_ids.shape[1]
+ audio_mask = torch.ones(total_length, dtype=torch.bool)
+ else:
+ input_ids = torch.cat([style_inputs, text_inputs, audio_inputs], dim=1)
+ labels = torch.cat([style_labels, text_labels, audio_labels], dim=1)
+ total_length = input_ids.shape[1]
+ audio_start_idx = style_inputs.shape[1] + text_inputs.shape[1]
+ audio_mask = torch.zeros(total_length, dtype=torch.bool)
+ audio_mask[audio_start_idx:] = True
+
+ return_dict = {
+ "input_ids": input_ids, # [C, L]
+ "labels": labels, # [C, L]
+ "audio_mask": audio_mask, # [L]
+ "length": total_length,
+ }
+
+ return return_dict
+
+
+class OmniVoiceSimpleSampleProcessor:
+ """
+ Handles the logic of processing a raw sample into tensors
+ (masking, tokenization, etc.).
+ This is a simpler version that does not include language, instructions,
+ or denoising prompts.
+ We do not use it for training as OmniVoiceSampleProcessor can cover this case.
+ We keep it as a reference implementation for users to understand the basic logics.
+ """
+
+ def __init__(
+ self,
+ text_tokenizer: Any,
+ num_channels: int,
+ audio_mask_id: int,
+ prompt_ratio_range: tuple,
+ mask_ratio_range: tuple,
+ drop_cond_ratio: float,
+ ):
+ self.text_tokenizer = text_tokenizer
+ self.num_channels = num_channels
+ self.audio_mask_id = audio_mask_id
+ self.prompt_ratio_range = prompt_ratio_range
+ self.mask_ratio_range = mask_ratio_range
+ self.drop_cond_ratio = drop_cond_ratio
+
+ def __call__(self, sample: Dict[str, Any]) -> Dict[str, Any]:
+ drop_cond = random.uniform(0, 1) < self.drop_cond_ratio
+ mask_ratio = random.uniform(*self.mask_ratio_range)
+
+ if drop_cond:
+ prompt_ratio = 0.0
+ else:
+ prompt_ratio = random.uniform(*self.prompt_ratio_range)
+
+ # --- Text ---
+ text = sample["label"]["text"]
+ text_inputs = self.text_tokenizer(
+ f"<|text_start|>{text}<|text_end|>", return_tensors="pt"
+ ).input_ids.repeat(self.num_channels, 1)
+ text_labels = torch.full(text_inputs.shape, -100) # Text does not compute loss
+
+ # --- Audio ---
+ audio_tokens = sample["audio_tokens"].long()
+
+ # Masking Logic
+ prompt_length = int(audio_tokens.shape[1] * prompt_ratio)
+ audio_inputs = audio_tokens.clone()
+ audio_labels = audio_tokens.clone()
+
+ # Apply masking
+ maskable_region = audio_tokens[:, prompt_length:]
+ token_mask = torch.rand(maskable_region.shape) < mask_ratio
+ audio_inputs[:, prompt_length:][token_mask] = self.audio_mask_id
+ audio_labels[:, prompt_length:][
+ ~token_mask
+ ] = -100 # Only compute loss on masked tokens
+
+ if not drop_cond:
+ # No loss on prompt region
+ audio_labels[:, :prompt_length] = -100
+
+ # --- Concatenation ---
+ if drop_cond:
+ input_ids = audio_inputs
+ labels = audio_labels
+ total_length = input_ids.shape[1]
+ audio_mask = torch.ones(total_length, dtype=torch.bool)
+ else:
+ input_ids = torch.cat([text_inputs, audio_inputs], dim=1)
+ labels = torch.cat([text_labels, audio_labels], dim=1)
+ total_length = input_ids.shape[1]
+ audio_start_idx = text_inputs.shape[1]
+ audio_mask = torch.zeros(total_length, dtype=torch.bool)
+ audio_mask[audio_start_idx:] = True
+
+ return_dict = {
+ "input_ids": input_ids, # [C, L]
+ "labels": labels, # [C, L]
+ "audio_mask": audio_mask, # [L]
+ "length": total_length,
+ }
+
+ return return_dict
diff --git a/omnivoice/eval/__init__.py b/omnivoice/eval/__init__.py
new file mode 100644
index 00000000..88e1e0a0
--- /dev/null
+++ b/omnivoice/eval/__init__.py
@@ -0,0 +1,4 @@
+import warnings
+
+# Suppress specific warnings from zhconv that are not relevant to WER calculation
+warnings.filterwarnings("ignore", category=UserWarning)
diff --git a/omnivoice/eval/models/ecapa_tdnn_wavlm.py b/omnivoice/eval/models/ecapa_tdnn_wavlm.py
new file mode 100644
index 00000000..1219fbcb
--- /dev/null
+++ b/omnivoice/eval/models/ecapa_tdnn_wavlm.py
@@ -0,0 +1,374 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+
+class ECAPA_TDNN_WAVLM(nn.Module):
+ def __init__(
+ self,
+ feat_dim=80,
+ channels=512,
+ emb_dim=192,
+ global_context_att=False,
+ sr=16000,
+ ssl_model_path=None,
+ ):
+ super().__init__()
+ self.sr = sr
+
+ if ssl_model_path is None:
+ self.feature_extract = torch.hub.load("s3prl/s3prl", "wavlm_large")
+ else:
+ self.feature_extract = torch.hub.load(
+ os.path.dirname(ssl_model_path),
+ "wavlm_local",
+ source="local",
+ ckpt=os.path.join(ssl_model_path, "wavlm_large.pt"),
+ )
+
+ if len(self.feature_extract.model.encoder.layers) == 24 and hasattr(
+ self.feature_extract.model.encoder.layers[23].self_attn,
+ "fp32_attention",
+ ):
+ self.feature_extract.model.encoder.layers[
+ 23
+ ].self_attn.fp32_attention = False
+ if len(self.feature_extract.model.encoder.layers) == 24 and hasattr(
+ self.feature_extract.model.encoder.layers[11].self_attn,
+ "fp32_attention",
+ ):
+ self.feature_extract.model.encoder.layers[
+ 11
+ ].self_attn.fp32_attention = False
+
+ self.feat_num = self.get_feat_num()
+ self.feature_weight = nn.Parameter(torch.zeros(self.feat_num))
+
+ self.instance_norm = nn.InstanceNorm1d(feat_dim)
+ # self.channels = [channels] * 4 + [channels * 3]
+ self.channels = [channels] * 4 + [1536]
+
+ self.layer1 = Conv1dReluBn(feat_dim, self.channels[0], kernel_size=5, padding=2)
+ self.layer2 = SE_Res2Block(
+ self.channels[0],
+ self.channels[1],
+ kernel_size=3,
+ stride=1,
+ padding=2,
+ dilation=2,
+ scale=8,
+ se_bottleneck_dim=128,
+ )
+ self.layer3 = SE_Res2Block(
+ self.channels[1],
+ self.channels[2],
+ kernel_size=3,
+ stride=1,
+ padding=3,
+ dilation=3,
+ scale=8,
+ se_bottleneck_dim=128,
+ )
+ self.layer4 = SE_Res2Block(
+ self.channels[2],
+ self.channels[3],
+ kernel_size=3,
+ stride=1,
+ padding=4,
+ dilation=4,
+ scale=8,
+ se_bottleneck_dim=128,
+ )
+
+ # self.conv = nn.Conv1d(self.channels[-1], self.channels[-1], kernel_size=1)
+ cat_channels = channels * 3
+ self.conv = nn.Conv1d(cat_channels, self.channels[-1], kernel_size=1)
+ self.pooling = AttentiveStatsPool(
+ self.channels[-1],
+ attention_channels=128,
+ global_context_att=global_context_att,
+ )
+ self.bn = nn.BatchNorm1d(self.channels[-1] * 2)
+ self.linear = nn.Linear(self.channels[-1] * 2, emb_dim)
+
+ def get_feat_num(self):
+ self.feature_extract.eval()
+ wav = [torch.randn(self.sr).to(next(self.feature_extract.parameters()).device)]
+ with torch.no_grad():
+ features = self.feature_extract(wav)
+ select_feature = features["hidden_states"]
+ if isinstance(select_feature, (list, tuple)):
+ return len(select_feature)
+ else:
+ return 1
+
+ def get_feat(self, x):
+ with torch.no_grad():
+ x = self.feature_extract([sample for sample in x])
+
+ x = x["hidden_states"]
+ if isinstance(x, (list, tuple)):
+ x = torch.stack(x, dim=0)
+ else:
+ x = x.unsqueeze(0)
+ norm_weights = (
+ F.softmax(self.feature_weight, dim=-1)
+ .unsqueeze(-1)
+ .unsqueeze(-1)
+ .unsqueeze(-1)
+ )
+ x = (norm_weights * x).sum(dim=0)
+ x = torch.transpose(x, 1, 2) + 1e-6
+
+ x = self.instance_norm(x)
+ return x
+
+ def forward(self, x):
+ x = self.get_feat(x)
+
+ out1 = self.layer1(x)
+ out2 = self.layer2(out1)
+ out3 = self.layer3(out2)
+ out4 = self.layer4(out3)
+
+ out = torch.cat([out2, out3, out4], dim=1)
+ out = F.relu(self.conv(out))
+ out = self.bn(self.pooling(out))
+ out = self.linear(out)
+
+ return out
+
+
+# part of the code is borrowed from https://github.com/lawlict/ECAPA-TDNN
+
+""" Res2Conv1d + BatchNorm1d + ReLU
+"""
+
+
+class Res2Conv1dReluBn(nn.Module):
+ """
+ in_channels == out_channels == channels
+ """
+
+ def __init__(
+ self,
+ channels,
+ kernel_size=1,
+ stride=1,
+ padding=0,
+ dilation=1,
+ bias=True,
+ scale=4,
+ ):
+ super().__init__()
+ assert channels % scale == 0, "{} % {} != 0".format(channels, scale)
+ self.scale = scale
+ self.width = channels // scale
+ self.nums = scale if scale == 1 else scale - 1
+
+ self.convs = []
+ self.bns = []
+ for i in range(self.nums):
+ self.convs.append(
+ nn.Conv1d(
+ self.width,
+ self.width,
+ kernel_size,
+ stride,
+ padding,
+ dilation,
+ bias=bias,
+ )
+ )
+ self.bns.append(nn.BatchNorm1d(self.width))
+ self.convs = nn.ModuleList(self.convs)
+ self.bns = nn.ModuleList(self.bns)
+
+ def forward(self, x):
+ out = []
+ spx = torch.split(x, self.width, 1)
+ for i in range(self.nums):
+ if i == 0:
+ sp = spx[i]
+ else:
+ sp = sp + spx[i]
+ # Order: conv -> relu -> bn
+ sp = self.convs[i](sp)
+ sp = self.bns[i](F.relu(sp))
+ out.append(sp)
+ if self.scale != 1:
+ out.append(spx[self.nums])
+ out = torch.cat(out, dim=1)
+
+ return out
+
+
+""" Conv1d + BatchNorm1d + ReLU
+"""
+
+
+class Conv1dReluBn(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size=1,
+ stride=1,
+ padding=0,
+ dilation=1,
+ bias=True,
+ ):
+ super().__init__()
+ self.conv = nn.Conv1d(
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride,
+ padding,
+ dilation,
+ bias=bias,
+ )
+ self.bn = nn.BatchNorm1d(out_channels)
+
+ def forward(self, x):
+ return self.bn(F.relu(self.conv(x)))
+
+
+""" The SE connection of 1D case.
+"""
+
+
+class SE_Connect(nn.Module):
+ def __init__(self, channels, se_bottleneck_dim=128):
+ super().__init__()
+ self.linear1 = nn.Linear(channels, se_bottleneck_dim)
+ self.linear2 = nn.Linear(se_bottleneck_dim, channels)
+
+ def forward(self, x):
+ out = x.mean(dim=2)
+ out = F.relu(self.linear1(out))
+ out = torch.sigmoid(self.linear2(out))
+ out = x * out.unsqueeze(2)
+
+ return out
+
+
+""" SE-Res2Block of the ECAPA-TDNN architecture.
+"""
+
+
+# def SE_Res2Block(channels, kernel_size, stride, padding, dilation, scale):
+# return nn.Sequential(
+# Conv1dReluBn(channels, 512, kernel_size=1, stride=1, padding=0),
+# Res2Conv1dReluBn(512, kernel_size, stride, padding, dilation, scale=scale),
+# Conv1dReluBn(512, channels, kernel_size=1, stride=1, padding=0),
+# SE_Connect(channels)
+# )
+
+
+class SE_Res2Block(nn.Module):
+ def __init__(
+ self,
+ in_channels,
+ out_channels,
+ kernel_size,
+ stride,
+ padding,
+ dilation,
+ scale,
+ se_bottleneck_dim,
+ ):
+ super().__init__()
+ self.Conv1dReluBn1 = Conv1dReluBn(
+ in_channels, out_channels, kernel_size=1, stride=1, padding=0
+ )
+ self.Res2Conv1dReluBn = Res2Conv1dReluBn(
+ out_channels, kernel_size, stride, padding, dilation, scale=scale
+ )
+ self.Conv1dReluBn2 = Conv1dReluBn(
+ out_channels, out_channels, kernel_size=1, stride=1, padding=0
+ )
+ self.SE_Connect = SE_Connect(out_channels, se_bottleneck_dim)
+
+ self.shortcut = None
+ if in_channels != out_channels:
+ self.shortcut = nn.Conv1d(
+ in_channels=in_channels,
+ out_channels=out_channels,
+ kernel_size=1,
+ )
+
+ def forward(self, x):
+ residual = x
+ if self.shortcut:
+ residual = self.shortcut(x)
+
+ x = self.Conv1dReluBn1(x)
+ x = self.Res2Conv1dReluBn(x)
+ x = self.Conv1dReluBn2(x)
+ x = self.SE_Connect(x)
+
+ return x + residual
+
+
+""" Attentive weighted mean and standard deviation pooling.
+"""
+
+
+class AttentiveStatsPool(nn.Module):
+ def __init__(self, in_dim, attention_channels=128, global_context_att=False):
+ super().__init__()
+ self.global_context_att = global_context_att
+
+ # Use Conv1d with stride == 1 rather than Linear,
+ # then we don't need to transpose inputs.
+ if global_context_att:
+ self.linear1 = nn.Conv1d(
+ in_dim * 3, attention_channels, kernel_size=1
+ ) # equals W and b in the paper
+ else:
+ self.linear1 = nn.Conv1d(
+ in_dim, attention_channels, kernel_size=1
+ ) # equals W and b in the paper
+ self.linear2 = nn.Conv1d(
+ attention_channels, in_dim, kernel_size=1
+ ) # equals V and k in the paper
+
+ def forward(self, x):
+
+ if self.global_context_att:
+ context_mean = torch.mean(x, dim=-1, keepdim=True).expand_as(x)
+ context_std = torch.sqrt(
+ torch.var(x, dim=-1, keepdim=True) + 1e-10
+ ).expand_as(x)
+ x_in = torch.cat((x, context_mean, context_std), dim=1)
+ else:
+ x_in = x
+
+ # DON'T use ReLU here! In experiments, I find ReLU hard to converge.
+ alpha = torch.tanh(self.linear1(x_in))
+ # alpha = F.relu(self.linear1(x_in))
+ alpha = torch.softmax(self.linear2(alpha), dim=2)
+ mean = torch.sum(alpha * x, dim=2)
+ residuals = torch.sum(alpha * (x**2), dim=2) - mean**2
+ std = torch.sqrt(residuals.clamp(min=1e-9))
+ return torch.cat([mean, std], dim=1)
diff --git a/omnivoice/eval/models/utmos.py b/omnivoice/eval/models/utmos.py
new file mode 100644
index 00000000..dca1d4ef
--- /dev/null
+++ b/omnivoice/eval/models/utmos.py
@@ -0,0 +1,370 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+UTMOS strong model.
+Implementation from https://github.com/tarepan/SpeechMOS
+
+"""
+
+import math
+from typing import List, Optional, Tuple
+
+import torch
+import torch.nn.functional as F
+from torch import Tensor, nn
+
+
+class UTMOS22Strong(nn.Module):
+ """Saeki_2022 paper's `UTMOS strong learner` inference model
+ (w/o Phoneme encoder)."""
+
+ def __init__(self):
+ """Init."""
+
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ feat_ssl, feat_domain_emb, feat_judge_emb, feat_rnn_h, feat_proj_h = (
+ 768,
+ 128,
+ 128,
+ 512,
+ 2048,
+ )
+ feat_cat = feat_ssl + feat_domain_emb + feat_judge_emb
+
+ # SSL/DataDomainEmb/JudgeIdEmb/BLSTM/Projection
+ self.wav2vec2 = Wav2Vec2Model()
+ self.domain_emb = nn.Parameter(
+ data=torch.empty(1, feat_domain_emb), requires_grad=False
+ )
+ self.judge_emb = nn.Parameter(
+ data=torch.empty(1, feat_judge_emb), requires_grad=False
+ )
+ self.blstm = nn.LSTM(
+ input_size=feat_cat,
+ hidden_size=feat_rnn_h,
+ batch_first=True,
+ bidirectional=True,
+ )
+ self.projection = nn.Sequential(
+ nn.Linear(feat_rnn_h * 2, feat_proj_h), nn.ReLU(), nn.Linear(feat_proj_h, 1)
+ )
+
+ def forward(self, wave: Tensor, sr: int) -> Tensor: # pylint: disable=invalid-name
+ """wave-to-score :: (B, T) -> (B,)"""
+
+ # Feature extraction :: (B, T) -> (B, Frame, Feat)
+ unit_series = self.wav2vec2(wave)
+ bsz, frm, _ = unit_series.size()
+
+ # DataDomain/JudgeId Embedding's Batch/Time expansion ::
+ # (B=1, Feat) -> (B=bsz, Frame=frm, Feat)
+ domain_series = self.domain_emb.unsqueeze(1).expand(bsz, frm, -1)
+ judge_series = self.judge_emb.unsqueeze(1).expand(bsz, frm, -1)
+
+ # Feature concatenation :: (B, Frame, Feat=f1) + (B, Frame, Feat=f2) +
+ # (B, Frame, Feat=f3) -> (B, Frame, Feat=f1+f2+f3)
+ cat_series = torch.cat([unit_series, domain_series, judge_series], dim=2)
+
+ # Frame-scale score estimation :: (B, Frame, Feat) -> (B, Frame, Feat)
+ # -> (B, Frame, Feat=1) - BLSTM/Projection
+ feat_series = self.blstm(cat_series)[0]
+ score_series = self.projection(feat_series)
+
+ # Utterance-scale score :: (B, Frame, Feat=1) -> (B, Feat=1)
+ # -> (B,) - Time averaging
+ utter_score = score_series.mean(dim=1).squeeze(1) * 2 + 3
+
+ return utter_score
+
+
+class Wav2Vec2Model(nn.Module):
+ """Wav2Vev2."""
+
+ def __init__(self):
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ feat_h1, feat_h2 = 512, 768
+ feature_enc_layers = (
+ [(feat_h1, 10, 5)] + [(feat_h1, 3, 2)] * 4 + [(feat_h1, 2, 2)] * 2
+ )
+
+ self.feature_extractor = ConvFeatureExtractionModel(
+ conv_layers=feature_enc_layers
+ ) # pyright: ignore [reportGeneralTypeIssues]
+ self.layer_norm = nn.LayerNorm(feat_h1)
+ self.post_extract_proj = nn.Linear(feat_h1, feat_h2)
+ self.dropout_input = nn.Dropout(0.1)
+ self.encoder = TransformerEncoder(feat_h2)
+
+ # Remnants
+ self.mask_emb = nn.Parameter(torch.FloatTensor(feat_h2))
+
+ def forward(self, source: Tensor):
+ """FeatureEncoder + ContextTransformer"""
+
+ # Feature encoding
+ features = self.feature_extractor(source)
+ features = features.transpose(1, 2)
+ features = self.layer_norm(features)
+ features = self.post_extract_proj(features)
+
+ # Context transformer
+ x = self.encoder(features)
+
+ return x
+
+
+class ConvFeatureExtractionModel(nn.Module):
+ """Feature Encoder."""
+
+ def __init__(self, conv_layers: List[Tuple[int, int, int]]):
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ def block(
+ n_in: int, n_out: int, k: int, stride: int, is_group_norm: bool = False
+ ):
+ if is_group_norm:
+ return nn.Sequential(
+ nn.Conv1d(n_in, n_out, k, stride=stride, bias=False),
+ nn.Dropout(p=0.0),
+ nn.GroupNorm(dim, dim, affine=True),
+ nn.GELU(),
+ )
+ else:
+ return nn.Sequential(
+ nn.Conv1d(n_in, n_out, k, stride=stride, bias=False),
+ nn.Dropout(p=0.0),
+ nn.GELU(),
+ )
+
+ in_d = 1
+ self.conv_layers = nn.ModuleList()
+ for i, params in enumerate(conv_layers):
+ (dim, k, stride) = params
+ self.conv_layers.append(block(in_d, dim, k, stride, is_group_norm=i == 0))
+ in_d = dim
+
+ def forward(self, series: Tensor) -> Tensor:
+ """:: (B, T) -> (B, Feat, Frame)"""
+
+ series = series.unsqueeze(1)
+ for conv in self.conv_layers:
+ series = conv(series)
+
+ return series
+
+
+class TransformerEncoder(nn.Module):
+ """Transformer."""
+
+ def build_encoder_layer(self, feat: int):
+ """Layer builder."""
+ return TransformerSentenceEncoderLayer(
+ embedding_dim=feat,
+ ffn_embedding_dim=3072,
+ num_attention_heads=12,
+ activation_fn="gelu",
+ dropout=0.1,
+ attention_dropout=0.1,
+ activation_dropout=0.0,
+ layer_norm_first=False,
+ )
+
+ def __init__(self, feat: int):
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ self.required_seq_len_multiple = 2
+
+ self.pos_conv = nn.Sequential(
+ *[
+ nn.utils.weight_norm(
+ nn.Conv1d(feat, feat, kernel_size=128, padding=128 // 2, groups=16),
+ name="weight",
+ dim=2,
+ ),
+ SamePad(128),
+ nn.GELU(),
+ ]
+ )
+ self.layer_norm = nn.LayerNorm(feat)
+ self.layers = nn.ModuleList([self.build_encoder_layer(feat) for _ in range(12)])
+
+ def forward(self, x: Tensor) -> Tensor:
+
+ x_conv = self.pos_conv(x.transpose(1, 2)).transpose(1, 2)
+ x = x + x_conv
+
+ x = self.layer_norm(x)
+
+ # pad to the sequence length dimension
+ x, pad_length = pad_to_multiple(
+ x, self.required_seq_len_multiple, dim=-2, value=0
+ )
+ if pad_length > 0:
+ padding_mask = x.new_zeros((x.size(0), x.size(1)), dtype=torch.bool)
+ padding_mask[:, -pad_length:] = True
+ else:
+ padding_mask, _ = pad_to_multiple(
+ None, self.required_seq_len_multiple, dim=-1, value=True
+ )
+
+ # :: (B, T, Feat) -> (T, B, Feat)
+ x = x.transpose(0, 1)
+ for layer in self.layers:
+ x = layer(x, padding_mask)
+ # :: (T, B, Feat) -> (B, T, Feat)
+ x = x.transpose(0, 1)
+
+ # undo paddding
+ if pad_length > 0:
+ x = x[:, :-pad_length]
+
+ return x
+
+
+class SamePad(nn.Module):
+ """Tail inverse padding."""
+
+ def __init__(self, kernel_size: int):
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+ assert kernel_size % 2 == 0, "`SamePad` now support only even kernel."
+
+ def forward(self, x: Tensor) -> Tensor:
+ return x[:, :, :-1]
+
+
+def pad_to_multiple(
+ x: Optional[Tensor], multiple: int, dim: int = -1, value: float = 0
+) -> Tuple[Optional[Tensor], int]:
+ """Tail padding."""
+ if x is None:
+ return None, 0
+ tsz = x.size(dim)
+ m = tsz / multiple
+ remainder = math.ceil(m) * multiple - tsz
+ if m.is_integer():
+ return x, 0
+ pad_offset = (0,) * (-1 - dim) * 2
+
+ return F.pad(x, (*pad_offset, 0, remainder), value=value), remainder
+
+
+class TransformerSentenceEncoderLayer(nn.Module):
+ """Transformer Encoder Layer used in BERT/XLM style pre-trained models."""
+
+ def __init__(
+ self,
+ embedding_dim: int,
+ ffn_embedding_dim: int,
+ num_attention_heads: int,
+ activation_fn: str,
+ dropout: float,
+ attention_dropout: float,
+ activation_dropout: float,
+ layer_norm_first: bool,
+ ) -> None:
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ assert layer_norm_first is False, "`layer_norm_first` is fixed to `False`"
+ assert activation_fn == "gelu", "`activation_fn` is fixed to `gelu`"
+
+ feat = embedding_dim
+
+ self.self_attn = MultiheadAttention(
+ feat, num_attention_heads, attention_dropout
+ )
+ self.dropout1 = nn.Dropout(dropout)
+ self.dropout2 = nn.Dropout(activation_dropout)
+ self.dropout3 = nn.Dropout(dropout)
+ self.fc1 = nn.Linear(feat, ffn_embedding_dim)
+ self.fc2 = nn.Linear(ffn_embedding_dim, feat)
+ self.self_attn_layer_norm = nn.LayerNorm(feat)
+ self.final_layer_norm = nn.LayerNorm(feat)
+
+ def forward(self, x: Tensor, self_attn_padding_mask: Optional[Tensor]):
+ # Res[Attn-Do]-LN
+ residual = x
+ x = self.self_attn(x, x, x, self_attn_padding_mask)
+ x = self.dropout1(x)
+ x = residual + x
+ x = self.self_attn_layer_norm(x)
+
+ # Res[SegFC-GELU-Do-SegFC-Do]-LN
+ residual = x
+ x = F.gelu(self.fc1(x)) # pyright: ignore [reportUnknownMemberType]
+ x = self.dropout2(x)
+ x = self.fc2(x)
+ x = self.dropout3(x)
+ x = residual + x
+ x = self.final_layer_norm(x)
+
+ return x
+
+
+class MultiheadAttention(nn.Module):
+ """Multi-headed attention."""
+
+ def __init__(self, embed_dim: int, num_heads: int, dropout: float):
+ super().__init__() # pyright: ignore [reportUnknownMemberType]
+
+ self.embed_dim, self.num_heads, self.p_dropout = embed_dim, num_heads, dropout
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=True)
+ self.k_proj = nn.Linear(embed_dim, embed_dim, bias=True)
+ self.v_proj = nn.Linear(embed_dim, embed_dim, bias=True)
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True)
+
+ def forward(
+ self,
+ query: Tensor,
+ key: Tensor,
+ value: Tensor,
+ key_padding_mask: Optional[Tensor],
+ ) -> Tensor:
+ """
+ Args:
+ query :: (T, B, Feat)
+ key_padding_mask :: (B, src_len) - mask to exclude keys that are pads
+ , where padding elements are indicated by 1s.
+ """
+ return F.multi_head_attention_forward(
+ query=query,
+ key=key,
+ value=value,
+ embed_dim_to_check=self.embed_dim,
+ num_heads=self.num_heads,
+ in_proj_weight=torch.empty([0]),
+ in_proj_bias=torch.cat(
+ (self.q_proj.bias, self.k_proj.bias, self.v_proj.bias)
+ ),
+ bias_k=None,
+ bias_v=None,
+ add_zero_attn=False,
+ dropout_p=self.p_dropout,
+ out_proj_weight=self.out_proj.weight,
+ out_proj_bias=self.out_proj.bias,
+ training=False,
+ key_padding_mask=key_padding_mask.bool()
+ if key_padding_mask is not None
+ else None,
+ need_weights=False,
+ use_separate_proj_weight=True,
+ q_proj_weight=self.q_proj.weight,
+ k_proj_weight=self.k_proj.weight,
+ v_proj_weight=self.v_proj.weight,
+ )[0]
diff --git a/omnivoice/eval/mos/utmos.py b/omnivoice/eval/mos/utmos.py
new file mode 100755
index 00000000..7d19a024
--- /dev/null
+++ b/omnivoice/eval/mos/utmos.py
@@ -0,0 +1,299 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Calculate UTMOS score with automatic Mean Opinion Score (MOS) prediction system
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import sys
+import traceback
+import warnings
+from concurrent.futures import ProcessPoolExecutor, as_completed
+
+import numpy as np
+import torch
+from tqdm import tqdm
+
+from omnivoice.eval.models.utmos import UTMOS22Strong
+from omnivoice.eval.utils import load_waveform
+from omnivoice.utils.data_utils import read_test_list
+
+warnings.filterwarnings("ignore")
+
+# Global variables for workers
+worker_model = None
+worker_device = None
+worker_sr = 16000
+
+
+def get_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="Calculate UTMOS score using UTMOS22Strong model."
+ )
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing evaluated speech files.",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ required=True,
+ help="Path to the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of our evaluation model repository."
+ "Download from https://huggingface.co/k2-fsa/TTS_eval_models."
+ "Will use 'tts_eval_models/mos/utmos22_strong_step7459_v1.pt'"
+ " in this script",
+ )
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where UTMOS information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu",
+ type=int,
+ default=1,
+ help="Number of worker processes to spawn per GPU.",
+ )
+ return parser
+
+
+def get_device(rank: int = 0) -> torch.device:
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+ return device
+
+
+def worker_init(
+ rank_queue,
+ model_path,
+):
+ """Initialize worker process with model and device."""
+ global worker_model, worker_device, worker_sr
+
+ # Limit CPU threads per worker
+ torch.set_num_threads(2)
+
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] [Worker %(process)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ rank = rank_queue.get() if rank_queue else -1
+
+ worker_device = get_device(rank)
+ worker_sr = 16000
+
+ logging.debug(f"Initializing UTMOS worker on {worker_device}")
+
+ # Initialize Model
+ worker_model = UTMOS22Strong()
+ try:
+ # Load weights to CPU first, then move to device
+ state_dict = torch.load(model_path, map_location="cpu")
+ worker_model.load_state_dict(state_dict)
+ except Exception as e:
+ logging.error(f"Failed to load model from {model_path}: {e}")
+ raise
+
+ worker_model.to(worker_device)
+ worker_model.eval()
+
+
+@torch.no_grad()
+def run_utmos_worker(file_idx, wav_path, language_name):
+ """Worker function to process a single audio file."""
+ try:
+ if not os.path.exists(wav_path):
+ return file_idx, wav_path, language_name, f"File not found: {wav_path}", "error"
+
+ # Load and preprocess waveform
+ speech = load_waveform(wav_path, worker_sr, device=worker_device)
+
+ # Compute score
+ # UTMOS expects input shape (Batch, Time)
+ score = worker_model(speech.unsqueeze(0), worker_sr)
+
+ return file_idx, wav_path, language_name, score.item(), "success"
+
+ except Exception as e:
+ error_detail = (
+ f"Error processing {wav_path}: {str(e)}\n"
+ f"Traceback:\n{traceback.format_exc()}"
+ )
+ return file_idx, wav_path, language_name, error_detail, "error"
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ # Main process thread setting
+ torch.set_num_threads(2)
+
+ mp.set_start_method("spawn", force=True)
+
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ # Validate inputs
+ if not os.path.isdir(args.wav_path):
+ logging.error(f"Invalid directory: {args.wav_path}")
+ sys.exit(1)
+
+ model_path = os.path.join(args.model_dir, "mos/utmos22_strong_step7459_v1.pt")
+ if not os.path.exists(model_path):
+ logging.error(f"Model file not found at {model_path}")
+ sys.exit(1)
+
+ # Scan directory for files
+ logging.info(f"Calculating UTMOS for {args.wav_path}")
+
+ wav_files = []
+ try:
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ language_name = s.get("language_name") or "unknown"
+ eval_wav_path = os.path.join(args.wav_path, f"{s['id']}.{args.extension}")
+ wav_files.append((eval_wav_path, language_name))
+ except Exception as e:
+ raise ValueError(f"Error reading test list {args.test_list}: {e}")
+
+ # Setup Parallel Processing
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_procs = num_gpus * args.nj_per_gpu
+
+ logging.info(
+ f"Starting evaluation with {total_procs} processes on {num_gpus} GPUs."
+ )
+
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+
+ for rank in list(range(num_gpus)) * args.nj_per_gpu:
+ rank_queue.put(rank)
+
+ scores = []
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ fout = open(args.decode_path, "w", encoding="utf8")
+ logging.info(f"Saving detailed UTMOS results to: {args.decode_path}")
+ fout.write("Name\tUTMOS\n")
+
+ try:
+ with ProcessPoolExecutor(
+ max_workers=total_procs,
+ initializer=worker_init,
+ initargs=(
+ rank_queue,
+ model_path,
+ ),
+ ) as executor:
+ futures = []
+ for i, (wav_path, language_name) in enumerate(wav_files):
+ futures.append(
+ executor.submit(run_utmos_worker, i, wav_path, language_name)
+ )
+
+ pbar = tqdm(
+ as_completed(futures), total=len(wav_files), desc="Evaluating UTMOS"
+ )
+ lang_stats = {}
+ for future in pbar:
+ idx, path, language_name, result, status = future.result()
+ if status == "success":
+ if language_name not in lang_stats:
+ lang_stats[language_name] = []
+ lang_stats[language_name].append(result)
+ scores.append(result)
+ if fout:
+ if language_name == "unknown":
+ fout.write(f"{os.path.basename(path)}\t{result:.2f}\n")
+ else:
+ fout.write(
+ f"{language_name}\t{os.path.basename(path)}\t{result:.2f}\n"
+ )
+ else:
+ pbar.write(f"!!! FAILED [File {idx}]: {path} | {result}")
+
+ except (Exception, KeyboardInterrupt) as e:
+ logging.critical(
+ f"An unrecoverable error occurred: {e}. Terminating all processes."
+ )
+ detailed_error_info = traceback.format_exc()
+ logging.error(f"--- DETAILED TRACEBACK ---\n{detailed_error_info}")
+ sys.exit(1)
+
+ print("-" * 50)
+
+ if len(lang_stats) > 1:
+ lang_scores = []
+ for lang in sorted(lang_stats.keys()):
+ l_scores = lang_stats[lang]
+ l_avg = np.mean(l_scores)
+ lang_scores.append(l_scores)
+ l_count = len(l_scores)
+ logging.info(f"[{lang}] UTMOS score: {l_avg:.3f} ({l_count} samples)")
+ if fout:
+ fout.write(f"[{lang}] UTMOS: {l_avg:.3f} ({l_count} samples)\n")
+ logging.info(
+ f"Macro-average UTMOS over {len(lang_stats)} languages: "
+ f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}"
+ )
+ if fout:
+ fout.write(
+ f"\nMacro-average UTMOS over {len(lang_stats)} languages: "
+ f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}\n"
+ )
+
+ if scores:
+ avg_score = np.mean(scores)
+ logging.info(f"Processed {len(scores)}/{len(wav_files)} files.")
+ logging.info(f"UTMOS score: {avg_score:.2f}")
+ if fout:
+ fout.write(f"\nAverage UTMOS: {avg_score:.2f}\n")
+ else:
+ logging.error("No valid scores computed.")
+ print("-" * 50)
+
+ if fout:
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/speaker_similarity/sim.py b/omnivoice/eval/speaker_similarity/sim.py
new file mode 100755
index 00000000..365d1eef
--- /dev/null
+++ b/omnivoice/eval/speaker_similarity/sim.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Computes speaker similarity (SIM-o) using a WavLM-based
+ ECAPA-TDNN speaker verification model.
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import sys
+import traceback
+import warnings
+from concurrent.futures import ProcessPoolExecutor, as_completed
+
+import numpy as np
+import torch
+from tqdm import tqdm
+
+from omnivoice.eval.models.ecapa_tdnn_wavlm import ECAPA_TDNN_WAVLM
+from omnivoice.eval.utils import load_waveform
+from omnivoice.utils.data_utils import read_test_list
+
+warnings.filterwarnings("ignore")
+
+# Global variables for workers
+worker_model = None
+worker_device = None
+worker_sr = 16000
+
+
+def get_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="Calculate speaker similarity (SIM-o) score."
+ )
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing evaluated speech files.",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ required=True,
+ help="Path to the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of our evaluation model repository."
+ "Download from https://huggingface.co/k2-fsa/TTS_eval_models."
+ "Will use 'tts_eval_models/speaker_similarity/wavlm_large_finetune.pth'"
+ "and 'tts_eval_models/speaker_similarity/wavlm_large/' in this script",
+ )
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files.",
+ )
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where SIM-o information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu",
+ type=int,
+ default=1,
+ help="Number of worker processes to spawn per GPU.",
+ )
+ return parser
+
+
+def get_device(rank: int = 0) -> torch.device:
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+ return device
+
+
+def worker_init(
+ rank_queue,
+ sv_model_path,
+ ssl_model_path,
+):
+ """Initialize worker process with model and device."""
+ global worker_model, worker_device, worker_sr
+
+ torch.set_num_threads(2)
+
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] [Worker %(process)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ rank = rank_queue.get() if rank_queue else -1
+
+ worker_device = get_device(rank)
+ worker_sr = 16000
+
+ logging.debug(f"Initializing SIM-o worker on {worker_device}")
+ # Temporarily suppress INFO logs to hide verbose WavLM config
+ logging.disable(logging.INFO)
+
+ # Initialize Model
+ try:
+ worker_model = ECAPA_TDNN_WAVLM(
+ feat_dim=1024,
+ channels=512,
+ emb_dim=256,
+ sr=worker_sr,
+ ssl_model_path=ssl_model_path,
+ )
+ state_dict = torch.load(
+ sv_model_path, map_location=lambda storage, loc: storage
+ )
+ worker_model.load_state_dict(state_dict["model"], strict=False)
+ worker_model.to(worker_device)
+ worker_model.eval()
+ finally:
+ # Restore normal logging
+ logging.disable(logging.NOTSET)
+
+
+@torch.no_grad()
+def get_embedding(wav_path: str) -> torch.Tensor:
+ """Extract embedding for a single file."""
+ speech = load_waveform(wav_path, worker_sr, device=worker_device, max_seconds=120)
+ return worker_model([speech])
+
+
+def run_similarity_worker(line_idx, sample, wav_dir, extension):
+ """Worker function to process a single pair."""
+ try:
+ wav_name = sample["id"]
+ ref_wav_path = sample["ref_audio"]
+ language_name = sample.get("language_name") or "unknown"
+ eval_wav_path = os.path.join(wav_dir, f"{wav_name}.{extension}")
+
+ if not os.path.exists(ref_wav_path):
+ return line_idx, f"Reference not found: {ref_wav_path}", None, "error"
+ if not os.path.exists(eval_wav_path):
+ return line_idx, f"Eval wav not found: {eval_wav_path}", None, "error"
+
+ # Compute embeddings pair-wise
+ ref_emb = get_embedding(ref_wav_path)
+ eval_emb = get_embedding(eval_wav_path)
+
+ # Cosine Similarity
+ similarity = torch.nn.functional.cosine_similarity(ref_emb, eval_emb, dim=-1)
+
+ return (
+ line_idx,
+ (ref_wav_path, eval_wav_path, language_name),
+ similarity.item(),
+ "success",
+ )
+
+ except Exception as e:
+ error_detail = f"Error: {str(e)}\nTraceback:\n{traceback.format_exc()}"
+ return line_idx, str(sample), error_detail, "error"
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ # Main process thread setting
+ torch.set_num_threads(2)
+
+ mp.set_start_method("spawn", force=True)
+
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ # Prepare paths
+ sv_model_path = os.path.join(
+ args.model_dir, "speaker_similarity/wavlm_large_finetune.pth"
+ )
+ ssl_model_path = os.path.join(args.model_dir, "speaker_similarity/wavlm_large/")
+
+ if not os.path.exists(sv_model_path) or not os.path.exists(ssl_model_path):
+ logging.error("Model files not found. Please check --model-dir.")
+ sys.exit(1)
+
+ logging.info(f"Calculating SIM-o for {args.wav_path}")
+ # Read list
+ samples = read_test_list(args.test_list)
+
+ # Setup Parallel Processing
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_procs = num_gpus * args.nj_per_gpu
+
+ logging.info(
+ f"Starting evaluation with {total_procs} processes " f"on {num_gpus} GPUs."
+ )
+
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+
+ for rank in list(range(num_gpus)) * args.nj_per_gpu:
+ rank_queue.put(rank)
+
+ scores = []
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ fout = open(args.decode_path, "w", encoding="utf8")
+ logging.info(f"Saving detailed SIM-o results to: {args.decode_path}")
+ fout.write("Prompt-path\tEval-path\tSIM-o\n")
+
+ try:
+ with ProcessPoolExecutor(
+ max_workers=total_procs,
+ initializer=worker_init,
+ initargs=(
+ rank_queue,
+ sv_model_path,
+ ssl_model_path,
+ ),
+ ) as executor:
+ futures = []
+ for i, sample in enumerate(samples):
+ futures.append(
+ executor.submit(
+ run_similarity_worker, i, sample, args.wav_path, args.extension
+ )
+ )
+
+ pbar = tqdm(
+ as_completed(futures), total=len(samples), desc="Evaluating SIM-o"
+ )
+
+ lang_stats = {}
+
+ for future in pbar:
+ idx, context, result, status = future.result()
+ if status == "success":
+ prompt_path, eval_path, lang = context
+ scores.append(result)
+
+ # Accumulate per-language
+ if lang not in lang_stats:
+ lang_stats[lang] = []
+ lang_stats[lang].append(result)
+
+ if fout:
+ if lang == "unknown":
+ fout.write(f"{prompt_path}\t{eval_path}\t{result:.2f}\n")
+ else:
+ fout.write(
+ f"{lang}\t{context[0]}\t{context[1]}\t{result:.2f}\n"
+ )
+ else:
+ pbar.write(f"!!! FAILED [Line {idx}]: {context} | Error: {result}")
+
+ except (Exception, KeyboardInterrupt) as e:
+ logging.critical(
+ f"An unrecoverable error occurred: {e}. " f"Terminating all processes."
+ )
+ detailed_error_info = traceback.format_exc()
+ logging.error(f"--- DETAILED TRACEBACK ---\n{detailed_error_info}")
+ sys.exit(1)
+
+ print("-" * 50)
+ if len(lang_stats) > 1:
+ lang_scores = []
+ for lang in sorted(lang_stats.keys()):
+ l_scores = lang_stats[lang]
+ l_avg = np.mean(l_scores)
+ lang_scores.append(l_scores)
+ l_count = len(l_scores)
+ logging.info(f"[{lang}] SIM-o score: {l_avg:.3f} ({l_count} pairs)")
+ if fout:
+ fout.write(f"[{lang}] SIM-o: {l_avg:.3f} ({l_count} pairs)\n")
+ logging.info(
+ f"Macro-average SIM-o over {len(lang_stats)} languages: "
+ f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}"
+ )
+ if fout:
+ fout.write(
+ f"\nMacro-average SIM-o over {len(lang_stats)} languages: "
+ f"{np.mean([np.mean(ls) for ls in lang_scores]):.3f}\n"
+ )
+
+ if scores:
+ avg_score = np.mean(scores)
+ logging.info(f"Processed {len(scores)}/{len(samples)} pairs.")
+ logging.info(f"SIM-o score: {avg_score:.3f}")
+ if fout:
+ fout.write(f"\nAverage SIM-o: {avg_score:.3f}\n")
+ else:
+ logging.error("No valid scores computed.")
+ if fout:
+ fout.close()
+ print("-" * 50)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/utils.py b/omnivoice/eval/utils.py
new file mode 100644
index 00000000..5f48b6f3
--- /dev/null
+++ b/omnivoice/eval/utils.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+from typing import Optional
+
+import librosa
+import soundfile as sf
+import torch
+
+
+def load_waveform(
+ fname: str,
+ sample_rate: int,
+ dtype: str = "float32",
+ device: torch.device = torch.device("cpu"),
+ return_numpy: bool = False,
+ max_seconds: Optional[float] = None,
+) -> torch.Tensor:
+ """
+ Load an audio file, preprocess it, and convert to a PyTorch tensor.
+
+ Args:
+ fname (str): Path to the audio file.
+ sample_rate (int): Target sample rate for resampling.
+ dtype (str, optional): Data type to load audio as (default: "float32").
+ device (torch.device, optional): Device to place the resulting tensor
+ on (default: CPU).
+ return_numpy (bool): If True, returns a NumPy array instead of a
+ PyTorch tensor.
+ max_seconds (float): Maximum length (seconds) of the audio tensor.
+ If the audio is longer than this, it will be truncated.
+
+ Returns:
+ torch.Tensor: Processed audio waveform as a PyTorch tensor,
+ with shape (num_samples,).
+
+ Notes:
+ - If the audio is stereo, it will be converted to mono by averaging channels.
+ - If the audio's sample rate differs from the target, it will be resampled.
+ """
+ # Load audio file with specified data type
+ wav_data, sr = sf.read(fname, dtype=dtype)
+
+ # Convert stereo to mono if necessary
+ if len(wav_data.shape) == 2:
+ wav_data = wav_data.mean(1)
+
+ # Resample to target sample rate if needed
+ if sr != sample_rate:
+ wav_data = librosa.resample(wav_data, orig_sr=sr, target_sr=sample_rate)
+
+ if max_seconds is not None:
+ # Trim to max length
+ max_length = int(sample_rate * max_seconds)
+ if len(wav_data) > max_length:
+ wav_data = wav_data[:max_length]
+ logging.warning(
+ f"Wav file {fname} is longer than {max_seconds}s, "
+ f"truncated to {max_seconds}s to avoid OOM."
+ )
+ if return_numpy:
+ return wav_data
+ else:
+ wav_data = torch.from_numpy(wav_data)
+ return wav_data.to(device)
diff --git a/omnivoice/eval/wer/common.py b/omnivoice/eval/wer/common.py
new file mode 100644
index 00000000..c081fbd9
--- /dev/null
+++ b/omnivoice/eval/wer/common.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Shared utilities for WER evaluation scripts.
+"""
+import logging
+
+import numpy as np
+from jiwer import compute_measures
+
+
+def process_one(hypothesis: str, truth: str, post_process, lang: str = None) -> dict:
+ """
+ Computes WER and related metrics for a single hypothesis-truth pair.
+
+ Args:
+ hypothesis (str): The transcribed text from the ASR model.
+ truth (str): The ground truth transcript.
+ post_process (callable): Text normalization function defined by each script.
+ Signature: post_process(text, lang) or post_process(text).
+ lang (str): The language code for post_process. Pass None if post_process
+ does not accept a lang argument.
+
+ Returns:
+ dict: A dict containing:
+ - truth (str): Post-processed ground truth text.
+ - hypothesis (str): Post-processed hypothesis text.
+ - wer (float): Word Error Rate.
+ - substitutions (int): Number of substitutions.
+ - deletions (int): Number of deletions.
+ - insertions (int): Number of insertions.
+ - word_num (int): Number of words in the post-processed ground truth.
+ """
+ if lang is not None:
+ truth_processed = post_process(truth, lang)
+ hypothesis_processed = post_process(hypothesis, lang)
+ else:
+ truth_processed = post_process(truth)
+ hypothesis_processed = post_process(hypothesis)
+ measures = compute_measures(truth_processed, hypothesis_processed)
+ word_num = len(truth_processed.split(" "))
+ return {
+ "truth": truth_processed,
+ "hypo": hypothesis_processed,
+ "wer": measures["wer"],
+ "substitutions": measures["substitutions"],
+ "deletions": measures["deletions"],
+ "insertions": measures["insertions"],
+ "word_num": word_num,
+ }
+
+
+def log_metrics(fout, prefix, i_list, d_list, s_list, w_total, ndigits=2):
+ """Log weighted WER metrics for a subset of results."""
+ metrics_wer = round(
+ (np.sum(s_list) + np.sum(d_list) + np.sum(i_list)) / w_total * 100, ndigits
+ )
+ metrics_inse = np.sum(i_list)
+ metrics_dele = np.sum(d_list)
+ metrics_subs = np.sum(s_list)
+
+ logging.info(f"{prefix} WER: {metrics_wer}%")
+ logging.info(
+ f"{prefix} Errors: {metrics_inse} ins, {metrics_dele} del, "
+ f"{metrics_subs} sub / {w_total} words"
+ )
+ if fout:
+ fout.write(f"{prefix} WER: {metrics_wer}%\n")
+ fout.write(
+ f"{prefix} Errors: {metrics_inse} ins, {metrics_dele} del, "
+ f"{metrics_subs} sub / {w_total} words\n"
+ )
+ return metrics_wer
diff --git a/omnivoice/eval/wer/fleurs.py b/omnivoice/eval/wer/fleurs.py
new file mode 100755
index 00000000..b1899afc
--- /dev/null
+++ b/omnivoice/eval/wer/fleurs.py
@@ -0,0 +1,517 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Computes word error rate (WER) for FLEURS multilingual evaluation.
+
+Uses omnilingual-asr for ASR transcription across 100+ languages.
+Requires a separate environment with ``omnilingual_asr`` installed.
+
+Usage:
+ python3 omnivoice/eval/wer/fleurs.py \\
+ --wav-path results/fleurs \\
+ --test-list test.jsonl \\
+ --decode-path results/fleurs.wer.log \\
+ --model-card omniASR_LLM_Unlimited_7B_v2 \\
+ --chunk-size 100 --batch-size 50
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import re
+import sys
+import traceback
+import types
+from collections import defaultdict
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from pathlib import Path
+from typing import List, Union
+
+import numpy as np
+import torch
+from tqdm import tqdm
+
+try:
+ from omnilingual_asr.models.inference.pipeline import ASRInferencePipeline
+ from omnilingual_asr.models.wav2vec2_llama.lang_ids import supported_langs
+except ImportError:
+ logging.error("Please install omnilingual_asr first.")
+ exit(1)
+
+# omnilingual-asr may pull a transformers version that lacks
+# HiggsAudioV2TokenizerModel. Pre-register stubs to bypass
+# omnivoice/__init__.py heavy imports.
+if "omnivoice" not in sys.modules:
+ _root = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
+ for _name in (
+ "omnivoice",
+ "omnivoice.eval",
+ "omnivoice.eval.wer",
+ "omnivoice.utils",
+ ):
+ if _name not in sys.modules:
+ _m = types.ModuleType(_name)
+ _m.__path__ = [os.path.join(_root, *_name.split(".")[1:])]
+ _m.__package__ = _name
+ sys.modules[_name] = _m
+
+from omnivoice.eval.wer.common import log_metrics, process_one
+from omnivoice.eval.wer.text_norm_omni import text_normalize
+from omnivoice.utils.data_utils import read_test_list
+
+# --- Global variables for worker processes ---
+worker_pipe = None
+worker_device = None
+
+
+# fix mismatched language codes between OmniVoice and Omnilingual-ASR model
+rename = {
+ "et": "ekk",
+ "ms": "zsm",
+ "sw": "swh",
+ "npi": "nep",
+}
+
+
+def read_language_mapping_from_tsv(
+ mapping_path: Path,
+) -> dict[str, Union[str, List[str]]]:
+ with open(mapping_path, "r", encoding="utf-8") as f:
+ _ = f.readline() # Skip header
+ language_mapping = {}
+ for line in f:
+ parts = line.strip().split("\t")
+ mixed_id, language_name, iso_639_3_id, duration = parts
+ language_mapping[iso_639_3_id] = mixed_id
+ return language_mapping
+
+
+iso_639_3_id_to_mixed_id = read_language_mapping_from_tsv(
+ Path(f"{os.path.dirname(__file__)}/../../../docs/lang_id_name_map.tsv")
+)
+
+mixed_id_to_omnilingual_asr_lang = {}
+
+for lang in supported_langs:
+ if lang in ("cmn_Hant",):
+ continue
+ iso_639_3_lang_code = lang.split("_")[0]
+ if iso_639_3_lang_code in iso_639_3_id_to_mixed_id:
+ mixed_id = iso_639_3_id_to_mixed_id[iso_639_3_lang_code]
+ mixed_id_to_omnilingual_asr_lang[mixed_id] = lang
+ else:
+ mixed_id_to_omnilingual_asr_lang[iso_639_3_lang_code] = lang
+
+
+def clean_cjk_spaces(text):
+ """
+ Removes spaces adjacent to Chinese and Japanese characters while preserving
+ meaningful spaces in English or other languages (like Korean).
+ """
+
+ # Define CJK (Chinese, Japanese) Unicode ranges
+ # \u4e00-\u9fff: CJK Unified Ideographs (Chinese)
+ # \u3040-\u309f: Hiragana (Japanese)
+ # \u30a0-\u30ff: Katakana (Japanese)
+ # \u3000-\u303f: CJK Symbols and Punctuation
+ cjk_range = r"\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3000-\u303f"
+
+ # 1. Remove spaces between two CJK characters
+ # Example: "我 爱 你" -> "我爱你"
+ text = re.sub(f"([{cjk_range}])\\s+([{cjk_range}])", r"\1\2", text)
+
+ # 2. Remove spaces between a CJK character and a non-CJK character (English/Numbers)
+ # Example: "我 爱 you" -> "我爱you"
+ text = re.sub(f"([{cjk_range}])\\s+", r"\1", text)
+ text = re.sub(f"\\s+([{cjk_range}])", r"\1", text)
+
+ # 3. Collapse multiple spaces into one for the remaining parts (e.g., English words)
+ text = re.sub(r"\s+", " ", text)
+
+ return text.strip()
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Computes WER with Whisper.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing speech files.",
+ )
+
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where WER information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--model-card",
+ type=str,
+ default="omniASR_LLM_7B",
+ help="Model card name for OmniASR (e.g., omniASR_LLM_7B) or local path.",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ default="test.jsonl",
+ help="path of the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--lang",
+ type=str,
+ default=None,
+ help="""Language code to evaluate (e.g., 'en' for English, 'zh' for Chinese).
+ If not provided, the script will evaluate all languages found in the test list.
+ If specified, only samples of the given language will be evaluated.
+ """,
+ )
+ parser.add_argument(
+ "--batch-size",
+ type=int,
+ default=8,
+ help="Batch size for decoding with the Hugging Face pipeline.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu", type=int, default=1, help="Number of workers per GPU."
+ )
+ parser.add_argument(
+ "--chunk-size",
+ type=int,
+ default=300,
+ help="Number of samples per task chunk sent to workers.",
+ )
+ return parser
+
+
+def load_omni_model(model_card, device):
+ logging.info(f"Loading OmniASR model ({model_card}) on {device}...")
+ try:
+ pipeline = ASRInferencePipeline(model_card=model_card, device=str(device))
+ return pipeline
+ except Exception as e:
+ logging.error(f"Failed to load OmniASR pipeline: {e}")
+ return None
+
+
+def process_init(rank_queue, model_card):
+ """
+ Initializer for each worker process.
+ """
+ global worker_pipe, worker_device
+
+ # Configure threads constraint
+ torch.set_num_threads(2)
+
+ try:
+ rank = rank_queue.get(timeout=10)
+ except Exception:
+ raise RuntimeError("Failed to get GPU rank from queue.")
+
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ worker_device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+ try:
+ # Using the model_card argument
+ worker_pipe = load_omni_model(model_card, worker_device)
+ if worker_pipe is None:
+ raise RuntimeError("Model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load model on {worker_device}: {e}")
+ raise e
+
+
+def post_process(text: str, lang: str) -> str:
+ """
+ Cleans and normalizes text for WER calculation.
+ Args:
+ text (str): The input text to be processed.
+ lang (str): The language of the input text.
+
+ Returns:
+ str: The cleaned and normalized text.
+ """
+ lang_id = lang[:3] # Extract ISO 639-3 code (e.g., 'eng' from 'eng_Latn')
+ text = text_normalize(
+ text,
+ iso_code=lang_id,
+ lower_case=True,
+ remove_numbers=False,
+ remove_brackets=False,
+ )
+ text = clean_cjk_spaces(text)
+ text = text.replace(" ", "|")
+ text = " ".join([x for x in text])
+ return text
+
+
+def run_eval_worker(data_chunk, language, batch_size):
+ """
+ Worker function to process a chunk of data.
+ Uses the global worker_pipe initialized by process_init.
+ """
+ global worker_pipe
+ if worker_pipe is None:
+ logging.error("Worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ # Prepare batch lists for OmniASR
+ audio_paths = [item["wav_path"] for item in data_chunk]
+
+ # OmniASR expects explicit language codes for each file if not auto-detected.
+ # Using the language passed to the worker function, or item specific language
+ # Assuming item['lang_id'] is compatible (e.g., 'en', 'zh', 'arb_Arab')
+ # If the model needs full tokens like 'en_Latn', conversion might be needed here depending on input data.
+ lang_list = [item.get("lang_id", language) for item in data_chunk]
+
+ # Use the pipeline to infer batch
+ # OmniASR pipeline.transcribe returns a list of strings
+ transcriptions = worker_pipe.transcribe(
+ audio_paths, lang=lang_list, batch_size=batch_size
+ )
+
+ for i, hypo_text in enumerate(transcriptions):
+ ref_item = data_chunk[i]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+ lang_id = ref_item.get("lang_id")
+ lang_name = ref_item.get("lang_name")
+
+ m = process_one(hypo_text, truth, post_process, lang_id)
+ m["wav_path"] = wav_path
+ m["lang_name"] = lang_name
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(
+ f"Worker failed on chunk (Lang: {language}):\n{traceback.format_exc()}"
+ )
+ return []
+
+ return metrics_buffer
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+ level=logging.INFO,
+ force=True,
+ )
+
+ # 1. Prepare Data
+ logging.info("Reading test list...")
+ data_by_lang = defaultdict(list)
+ total_files = 0
+ wav_root = Path(args.wav_path)
+
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ wav_path = str(wav_root / f"{s['id']}.{args.extension}")
+ if not os.path.exists(wav_path):
+ logging.warning(f"File missing: {wav_path}")
+ continue
+
+ lang_id = s.get("language_id") or "unknown"
+ if lang_id in rename:
+ lang_id = mixed_id_to_omnilingual_asr_lang[rename[lang_id]]
+ else:
+ lang_id = mixed_id_to_omnilingual_asr_lang[lang_id]
+ item = {
+ "wav_path": wav_path,
+ "truth_text": s["text"],
+ "lang_id": lang_id,
+ "lang_name": s.get("language_name") or "unknown",
+ }
+ if args.lang and s.get("language_id") != args.lang:
+ continue
+
+ data_by_lang[s.get("language_name") or "unknown"].append(item)
+
+ total_files += 1
+
+ logging.info(f"Total files: {total_files} in {len(data_by_lang)} languages.")
+
+ # 2. Worker config
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_workers = num_gpus * args.nj_per_gpu
+
+ mp.set_start_method("spawn", force=True)
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ rank_queue.put(rank)
+
+ # 3. Scheduling: Split languages into chunks
+ # This prevents one huge language from blocking a worker for too long,
+ # allows better load balancing across the pool.
+ tasks = []
+ chunk_size = args.chunk_size
+
+ for lang_name, items in data_by_lang.items():
+ # Slicing the list into chunks
+ for i in range(0, len(items), chunk_size):
+ chunk = items[i : i + chunk_size]
+ tasks.append({"chunk": chunk, "lang": lang_name})
+
+ logging.info(
+ f"Split data into {len(tasks)} chunks (size ~{chunk_size}). Spawning {total_workers} workers."
+ )
+
+ # 4. Execution
+ results = []
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init,
+ initargs=(rank_queue, args.model_card),
+ ) as executor:
+
+ futures = []
+ for task in tasks:
+ futures.append(
+ executor.submit(
+ run_eval_worker, task["chunk"], task["lang"], args.batch_size
+ )
+ )
+
+ # Unified progress bar
+ with tqdm(total=total_files, desc="Eval Progress", dynamic_ncols=True) as pbar:
+ for future in as_completed(futures):
+ try:
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+ except Exception as e:
+ logging.error(f"Task failed: {e}")
+
+ # 5. Metrics Aggregation
+ wers, inses, deles, subses = [], [], [], []
+ word_nums = 0
+
+ # Store metrics per language
+ lang_stats = {}
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ logging.info(f"Saving detailed WER results to: {args.decode_path}")
+ fout = open(args.decode_path, "w", encoding="utf-8")
+
+ for res in results:
+ wers.append(float(res["wer"]))
+ inses.append(float(res["insertions"]))
+ deles.append(float(res["deletions"]))
+ subses.append(float(res["substitutions"]))
+ word_nums += res["word_num"]
+
+ if fout:
+ fout.write(
+ f"{res['wav_path']}\t{res['wer']}\t{res['truth']}\t"
+ f"{res['hypo']}\t{res['insertions']}\t{res['deletions']}\t"
+ f"{res['substitutions']}\n"
+ )
+ lang_name = res["lang_name"]
+
+ # Per language stats
+ if lang_name not in lang_stats:
+ lang_stats[lang_name] = {
+ "inses": [],
+ "deles": [],
+ "subses": [],
+ "word_nums": 0,
+ }
+ lang_stats[lang_name]["inses"].append(float(res["insertions"]))
+ lang_stats[lang_name]["deles"].append(float(res["deletions"]))
+ lang_stats[lang_name]["subses"].append(float(res["substitutions"]))
+ lang_stats[lang_name]["word_nums"] += res["word_num"]
+
+ print("-" * 50)
+ # Log per-language stats
+ per_lang_wers = []
+ for lang in sorted(lang_stats.keys()):
+ stats = lang_stats[lang]
+ if stats["word_nums"] > 0:
+ lang_wer = log_metrics(
+ fout,
+ f"[{lang}]",
+ stats["inses"],
+ stats["deles"],
+ stats["subses"],
+ stats["word_nums"],
+ )
+ per_lang_wers.append(lang_wer)
+ print("-" * 50)
+
+ # Log Macro-average WER
+ if len(per_lang_wers) > 1:
+ macro_wer = np.mean(per_lang_wers)
+ logging.info(
+ f"Macro-average WER over {len(per_lang_wers)} languages: {macro_wer:.2f}%"
+ )
+ if fout:
+ fout.write(
+ f"Macro-average WER over {len(per_lang_wers)} languages: {macro_wer:.2f}%\n"
+ )
+ count_le_5 = sum(1 for w in per_lang_wers if w <= 5.0)
+ count_le_10 = sum(1 for w in per_lang_wers if w <= 10.0)
+ count_le_20 = sum(1 for w in per_lang_wers if w <= 20.0)
+
+ stats_msg = (
+ f"Languages with WER/CER <= 5%: {count_le_5}/{len(per_lang_wers)}\n"
+ f"Languages with WER/CER <= 10%: {count_le_10}/{len(per_lang_wers)}\n"
+ f"Languages with WER/CER <= 20%: {count_le_20}/{len(per_lang_wers)}"
+ )
+
+ logging.info("\n" + stats_msg)
+ if fout:
+ fout.write(stats_msg + "\n")
+
+ # Log overall stats
+ if word_nums > 0:
+ log_metrics(fout, "Overall", inses, deles, subses, word_nums)
+
+ if fout:
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/wer/hubert.py b/omnivoice/eval/wer/hubert.py
new file mode 100755
index 00000000..add1d258
--- /dev/null
+++ b/omnivoice/eval/wer/hubert.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Computes word error rate (WER) with Hubert models for LibriSpeech test sets.
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import re
+import traceback
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from pathlib import Path
+
+import numpy as np
+import torch
+from tqdm import tqdm
+
+from omnivoice.eval.utils import load_waveform
+from omnivoice.eval.wer.common import process_one
+from omnivoice.utils.data_utils import read_test_list
+
+# --- Global variables for worker processes ---
+worker_pipe = None
+worker_device = None
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Computes WER with Hubert-based ASR model.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing speech files.",
+ )
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where WER information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of our evaluation model repository."
+ "Download from https://huggingface.co/k2-fsa/TTS_eval_models."
+ "Will use 'tts_eval_models/wer/hubert-large-ls960-ft/'"
+ " in this script",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ default="transcript.jsonl",
+ help="path of the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--batch-size",
+ type=int,
+ default=16,
+ help="Batch size for decoding with the Hugging Face pipeline.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu", type=int, default=1, help="Number of workers per GPU."
+ )
+ return parser
+
+
+def process_init(rank_queue, model_dir):
+ global worker_pipe, worker_device
+
+ torch.set_num_threads(2)
+
+ try:
+ rank = rank_queue.get(timeout=10)
+ except Exception:
+ raise RuntimeError("Failed to get GPU rank from queue.")
+
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ worker_device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+ try:
+ worker_pipe = load_hubert_model(model_dir, worker_device)
+ if worker_pipe is None:
+ raise RuntimeError("Model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load model on {worker_device}: {e}")
+ raise e
+
+
+def load_hubert_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/hubert-large-ls960-ft/")
+ if not os.path.exists(model_path):
+ logging.error(
+ f"Hubert model not found at {model_path}. "
+ "Please download from https://huggingface.co/k2-fsa/TTS_eval_models"
+ )
+ return None
+
+ logging.debug(f"Loading Hubert-based ASR model on {device}...")
+ import transformers
+
+ # Suppress transformers logging
+ transformers.logging.set_verbosity_error()
+
+ pipe = transformers.pipeline(
+ "automatic-speech-recognition",
+ model=model_path,
+ device=device,
+ tokenizer=model_path,
+ )
+ return pipe
+
+
+def post_process(text: str) -> str:
+ """
+ Cleans and normalizes text for WER calculation.
+ Args:
+ text (str): The input text to be processed.
+
+ Returns:
+ str: The cleaned and normalized text.
+ """
+ text = text.replace("‘", "'").replace("’", "'")
+ text = re.sub(r"[^a-zA-Z0-9']", " ", text.lower())
+ text = re.sub(r"\s+", " ", text).strip()
+ return text
+
+
+def run_eval_worker(data_chunk, batch_size):
+ global worker_pipe
+ if worker_pipe is None:
+ logging.error("Worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ dataset = [
+ {
+ "array": load_waveform(
+ item["wav_path"], sample_rate=16000, return_numpy=True
+ ),
+ "sampling_rate": 16000,
+ }
+ for item in data_chunk
+ ]
+ generate_kwargs = {"language": "english", "task": "transcribe"}
+
+ iterator = worker_pipe(
+ dataset, generate_kwargs=generate_kwargs, batch_size=batch_size
+ )
+
+ for i, out in enumerate(iterator):
+ hypothesis = out["text"].strip()
+ ref_item = data_chunk[i]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+
+ m = process_one(hypothesis, truth, post_process)
+ m["wav_path"] = wav_path
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(f"Worker failed on chunk:\n{traceback.format_exc()}")
+ return []
+
+ return metrics_buffer
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+ level=logging.INFO,
+ force=True,
+ )
+
+ logging.info(f"Calculating WER for {args.wav_path}")
+
+ data_list = []
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ wav_full_path = str(Path(args.wav_path) / (s["id"] + "." + args.extension))
+ if not os.path.exists(wav_full_path):
+ logging.warning(f"File missing: {wav_full_path}")
+ continue
+ data_list.append(
+ {
+ "wav_path": wav_full_path,
+ "truth_text": s["text"],
+ }
+ )
+ total_files = len(data_list)
+
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_workers = num_gpus * args.nj_per_gpu
+
+ mp.set_start_method("spawn", force=True)
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ rank_queue.put(rank)
+
+ chunk_size = max(1, args.batch_size)
+ tasks = [data_list[i : i + chunk_size] for i in range(0, total_files, chunk_size)]
+
+ logging.info(
+ f"Split data into {len(tasks)} chunks (size ~{chunk_size}). "
+ f"Spawning {total_workers} workers."
+ )
+
+ results = []
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init,
+ initargs=(rank_queue, args.model_dir),
+ ) as executor:
+
+ futures = []
+ for chunk in tasks:
+ futures.append(executor.submit(run_eval_worker, chunk, args.batch_size))
+
+ with tqdm(total=total_files, desc="Eval Progress", dynamic_ncols=True) as pbar:
+ for future in as_completed(futures):
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+
+ wers, inses, deles, subses = [], [], [], []
+ word_nums = 0
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ fout = open(args.decode_path, "w", encoding="utf8")
+ logging.info(f"Saving detailed WER results to: {args.decode_path}")
+ fout.write(
+ "Name\tWER\tTruth\tHypothesis\tInsertions\tDeletions\tSubstitutions\n"
+ )
+
+ for res in results:
+ wers.append(float(res["wer"]))
+ inses.append(float(res["insertions"]))
+ deles.append(float(res["deletions"]))
+ subses.append(float(res["substitutions"]))
+ word_nums += res["word_num"]
+
+ if fout:
+ fout.write(
+ f"{res['wav_path']}\t{res['wer']}\t{res['truth']}\t"
+ f"{res['hypo']}\t{res['insertions']}\t{res['deletions']}\t"
+ f"{res['substitutions']}\n"
+ )
+
+ wer_weighted = (
+ round(
+ (np.sum(subses) + np.sum(deles) + np.sum(inses)) / word_nums * 100, 2
+ )
+ if word_nums > 0
+ else float("nan")
+ )
+
+ inse_sum = np.sum(inses)
+ dele_sum = np.sum(deles)
+ subs_sum = np.sum(subses)
+
+ print("-" * 50)
+ logging.info(f"Processed {len(results)}/{total_files} files.")
+ wer_info = f"WER: {wer_weighted}%"
+ detailed_info = (
+ f"Errors: {inse_sum} ins, {dele_sum} del, {subs_sum} sub / {word_nums} words"
+ )
+ logging.info(wer_info)
+ logging.info(detailed_info)
+ print("-" * 50)
+
+ if fout:
+ fout.write(wer_info + "\n" + detailed_info + "\n")
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/wer/minimax.py b/omnivoice/eval/wer/minimax.py
new file mode 100755
index 00000000..9fb6828b
--- /dev/null
+++ b/omnivoice/eval/wer/minimax.py
@@ -0,0 +1,596 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Computes word error rate (WER) with Whisper-large-v3 for English and
+Paraformer for Chinese. Intended to evaluate WERs on Seed-TTS test sets.
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import traceback
+from collections import defaultdict
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from pathlib import Path
+from typing import List, Union
+
+import numpy as np
+import torch
+import zhconv
+from tqdm import tqdm
+
+from omnivoice.eval.utils import load_waveform
+from omnivoice.eval.wer.common import log_metrics, process_one
+from omnivoice.eval.wer.text_norm_omni import text_normalize
+from omnivoice.utils.data_utils import read_test_list
+
+# --- Global variables for worker processes ---
+worker_pipe = None
+worker_paraformer = None
+worker_device = None
+
+
+def read_language_mapping_from_tsv(
+ mapping_path: Path,
+) -> dict[str, Union[str, List[str]]]:
+ with open(mapping_path, "r", encoding="utf-8") as f:
+ _ = f.readline() # Skip header
+ language_mapping = {}
+ for line in f:
+ parts = line.strip().split("\t")
+ mixed_id, language_name, iso_639_3_id, duration = parts
+ language_mapping[mixed_id] = iso_639_3_id
+ return language_mapping
+
+
+mixed_id_to_iso_639_3_id = read_language_mapping_from_tsv(
+ Path(f"{os.path.dirname(__file__)}/../../../docs/lang_id_name_map.tsv")
+)
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Computes WER with Whisper.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing speech files.",
+ )
+
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where WER information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of evaluation models repository. "
+ "Download from https://huggingface.co/k2-fsa/TTS_eval_models. ",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ default="test.jsonl",
+ help="path of the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--lang",
+ type=str,
+ default=None,
+ help="""Language code to evaluate (e.g., 'en' for English, 'zh' for Chinese).
+ If not provided, the script will evaluate all languages found in the test list.
+ If specified, only samples of the given language will be evaluated.
+ """,
+ )
+ parser.add_argument(
+ "--batch-size",
+ type=int,
+ default=16,
+ help="Batch size for decoding with the Hugging Face pipeline.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu", type=int, default=1, help="Number of workers per GPU."
+ )
+ parser.add_argument(
+ "--chunk-size",
+ type=int,
+ default=10,
+ help="Number of samples per task chunk sent to workers.",
+ )
+ return parser
+
+
+def load_whisper_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/whisper-large-v3/")
+ if not os.path.exists(model_path):
+ logging.error(f"Whisper model not found at {model_path}.")
+ return None
+
+ import transformers
+
+ # Suppress transformers logging
+ transformers.logging.set_verbosity_error()
+
+ logging.info(f"Loading Whisper model on {device}...")
+ pipe = transformers.pipeline(
+ "automatic-speech-recognition",
+ model=model_path,
+ chunk_length_s=30,
+ dtype=torch.float16 if "cuda" in str(device) else torch.float32,
+ device=device,
+ )
+ return pipe
+
+
+def load_paraformer_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/paraformer-zh/")
+ if not os.path.exists(model_path):
+ logging.error(f"Paraformer model not found at {model_path}.")
+ return None
+
+ logging.info(f"Loading Paraformer model on {device}...")
+
+ previous_level = logging.root.manager.disable
+ logging.disable(logging.CRITICAL)
+
+ try:
+ from funasr import AutoModel
+
+ model = AutoModel(
+ model=model_path,
+ device=str(device),
+ disable_update=True,
+ disable_pbar=True,
+ verbose=False,
+ )
+ finally:
+ logging.disable(previous_level)
+
+ return model
+
+
+def _worker_setup(rank_queue):
+ """Common worker setup: get rank, configure device and threads."""
+ global worker_device
+
+ torch.set_num_threads(2)
+
+ try:
+ rank = rank_queue.get(timeout=10)
+ except Exception:
+ raise RuntimeError("Failed to get GPU rank from queue.")
+
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ worker_device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+
+def process_init(rank_queue, model_dir):
+ """Initializer for Whisper worker processes."""
+ global worker_pipe
+
+ _worker_setup(rank_queue)
+
+ try:
+ worker_pipe = load_whisper_model(model_dir, worker_device)
+ if worker_pipe is None:
+ raise RuntimeError("Whisper model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load Whisper model on {worker_device}: {e}")
+ raise e
+
+
+def process_init_paraformer(rank_queue, model_dir):
+ """Initializer for Paraformer worker processes (Chinese evaluation)."""
+ global worker_paraformer
+
+ _worker_setup(rank_queue)
+
+ try:
+ worker_paraformer = load_paraformer_model(model_dir, worker_device)
+ if worker_paraformer is None:
+ raise RuntimeError("Paraformer model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load Paraformer model on {worker_device}: {e}")
+ raise e
+
+
+def post_process(text: str, lang: str) -> str:
+ """
+ Cleans and normalizes text for WER calculation.
+ Args:
+ text (str): The input text to be processed.
+ lang (str): The language of the input text.
+
+ Returns:
+ str: The cleaned and normalized text.
+ """
+ if lang != "unknown":
+
+ iso_639_3_code = mixed_id_to_iso_639_3_id[lang]
+ text = text_normalize(
+ text,
+ iso_code=iso_639_3_code,
+ lower_case=True,
+ remove_numbers=False,
+ remove_brackets=False,
+ )
+
+ if lang in ["zh", "yue"]:
+ text = zhconv.convert(text, "zh-cn")
+
+ # Processing spaces for languages using CER (consistent with the practice
+ # in paper Minimax-Speech), specifically: zh, yue, ja, ko, th, arb, vi, hi, el.
+ if lang in ("zh", "yue", "ja"):
+ # For languages where spaces are not semantically meaningful, remove spaces.
+ text = text.replace(" ", "")
+ text = " ".join([x for x in text])
+ elif lang in ("ko", "th", "arb", "vi", "hi", "el"):
+ # For languages where spaces are semantically meaningful, replace spaces with |.
+ text = text.replace(" ", "|")
+ text = " ".join([x for x in text])
+ text = text.lower()
+ return text.strip()
+
+
+class SpeechEvalDataset(torch.utils.data.Dataset):
+ def __init__(self, data_list):
+ self.data_list = data_list
+
+ def __len__(self):
+ return len(self.data_list)
+
+ def __getitem__(self, index):
+ item = self.data_list[index]
+ waveform = load_waveform(item["wav_path"], sample_rate=16000, return_numpy=True)
+ return {
+ "array": waveform,
+ "sampling_rate": 16000,
+ "truth_text": item["truth_text"],
+ }
+
+
+def run_eval_worker(data_chunk, language, batch_size):
+ """
+ Worker function to process a chunk of data.
+ Uses the global worker_pipe initialized by process_init.
+ """
+ global worker_pipe
+ if worker_pipe is None:
+ logging.error("Worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ dataset = SpeechEvalDataset(data_chunk)
+ if language != "unknown":
+ generate_kwargs = {"language": language, "task": "transcribe"}
+ else:
+ generate_kwargs = {"task": "transcribe"}
+
+ # Use the pipeline to infer batch
+ # Note: We iterate through the iterator returned by pipe
+ iterator = worker_pipe(
+ dataset, generate_kwargs=generate_kwargs, batch_size=batch_size
+ )
+
+ for i, out in enumerate(iterator):
+ hypothesis = out["text"].strip()
+
+ ref_item = data_chunk[i]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+ lang_id = ref_item.get("lang_id")
+ lang_name = ref_item.get("lang_name")
+
+ m = process_one(hypothesis, truth, post_process, lang_id)
+ m["wav_path"] = wav_path
+ m["lang_name"] = lang_name
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(
+ f"Worker failed on chunk (Lang: {language}):\n{traceback.format_exc()}"
+ )
+ return []
+
+ return metrics_buffer
+
+
+def run_eval_worker_paraformer(data_chunk, batch_size):
+ """
+ Worker function for Chinese evaluation using Paraformer.
+ Uses the global worker_paraformer initialized by process_init_paraformer.
+ """
+ global worker_paraformer
+ if worker_paraformer is None:
+ logging.error("Paraformer worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ wav_paths = [item["wav_path"] for item in data_chunk]
+
+ for i in range(0, len(wav_paths), batch_size):
+ batch_paths = wav_paths[i : i + batch_size]
+ res_batch = worker_paraformer.generate(
+ input=batch_paths, batch_size=batch_size, disable_pbar=True
+ )
+
+ for j, res in enumerate(res_batch):
+ hypothesis = res["text"]
+ ref_item = data_chunk[i + j]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+ lang_name = ref_item.get("lang_name")
+
+ m = process_one(hypothesis, truth, post_process, "zh")
+ m["wav_path"] = wav_path
+ m["lang_name"] = lang_name
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(f"Paraformer worker failed on chunk:\n{traceback.format_exc()}")
+ return []
+
+ return metrics_buffer
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+ level=logging.INFO,
+ force=True,
+ )
+
+ # 1. Prepare Data
+ logging.info("Reading test list...")
+ data_by_lang = defaultdict(list)
+ total_files = 0
+ wav_root = Path(args.wav_path)
+
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ wav_path = str(wav_root / f"{s['id']}.{args.extension}")
+ if not os.path.exists(wav_path):
+ logging.warning(f"File missing: {wav_path}")
+ continue
+
+ lang_id = s.get("language_id") or "unknown"
+ lang_name = s.get("language_name") or "unknown"
+
+ item = {
+ "wav_path": wav_path,
+ "truth_text": s["text"],
+ "lang_id": lang_id,
+ "lang_name": lang_name,
+ }
+ if args.lang and s.get("language_id") != args.lang:
+ continue
+
+ data_by_lang[lang_name].append(item)
+ total_files += 1
+
+ logging.info(f"Total files: {total_files} in {len(data_by_lang)} languages.")
+
+ # 2. Worker config
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_workers = num_gpus * args.nj_per_gpu
+
+ mp.set_start_method("spawn", force=True)
+ manager = mp.Manager()
+
+ # 3. Scheduling: Split data into Chinese (Paraformer) and non-Chinese (Whisper)
+ zh_items = []
+ non_zh_items = []
+ for lang_name, items in data_by_lang.items():
+ lang_id = items[0].get("lang_id", "") if items else ""
+ if lang_name == "Chinese" or (lang_id and lang_id.startswith("zh")):
+ zh_items.extend(items)
+ else:
+ non_zh_items.extend(items)
+
+ chunk_size = args.chunk_size
+
+ whisper_tasks = []
+ for i in range(0, len(non_zh_items), chunk_size):
+ chunk = non_zh_items[i : i + chunk_size]
+ lang_name = chunk[0].get("lang_name", "unknown")
+ whisper_tasks.append({"chunk": chunk, "lang": lang_name})
+
+ paraformer_tasks = []
+ for i in range(0, len(zh_items), chunk_size):
+ paraformer_tasks.append(zh_items[i : i + chunk_size])
+
+ logging.info(
+ f"Whisper tasks: {len(whisper_tasks)} chunks ({len(non_zh_items)} files). "
+ f"Paraformer tasks: {len(paraformer_tasks)} chunks ({len(zh_items)} files). "
+ f"Spawning {total_workers} workers per pool."
+ )
+
+ # 4. Execution — run Whisper and Paraformer pools sequentially
+ results = []
+
+ # 4a. Whisper pool for non-Chinese languages
+ if whisper_tasks:
+ whisper_rank_queue = manager.Queue()
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ whisper_rank_queue.put(rank)
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init,
+ initargs=(whisper_rank_queue, args.model_dir),
+ ) as executor:
+
+ futures = []
+ for task in whisper_tasks:
+ futures.append(
+ executor.submit(
+ run_eval_worker, task["chunk"], task["lang"], args.batch_size
+ )
+ )
+
+ with tqdm(
+ total=len(non_zh_items),
+ desc="Whisper Eval",
+ dynamic_ncols=True,
+ ) as pbar:
+ for future in as_completed(futures):
+ try:
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+ except Exception as e:
+ logging.error(f"Whisper task failed: {e}")
+
+ # 4b. Paraformer pool for Chinese
+ if paraformer_tasks:
+ para_rank_queue = manager.Queue()
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ para_rank_queue.put(rank)
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init_paraformer,
+ initargs=(para_rank_queue, args.model_dir),
+ ) as executor:
+
+ futures = []
+ for chunk in paraformer_tasks:
+ futures.append(
+ executor.submit(run_eval_worker_paraformer, chunk, args.batch_size)
+ )
+
+ with tqdm(
+ total=len(zh_items),
+ desc="Paraformer Eval",
+ dynamic_ncols=True,
+ ) as pbar:
+ for future in as_completed(futures):
+ try:
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+ except Exception as e:
+ logging.error(f"Paraformer task failed: {e}")
+
+ # 5. Metrics Aggregation
+ wers, inses, deles, subses = [], [], [], []
+ word_nums = 0
+
+ # Store metrics per language
+ lang_stats = {}
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ logging.info(f"Saving detailed WER results to: {args.decode_path}")
+ fout = open(args.decode_path, "w", encoding="utf-8")
+
+ for res in results:
+ wers.append(float(res["wer"]))
+ inses.append(float(res["insertions"]))
+ deles.append(float(res["deletions"]))
+ subses.append(float(res["substitutions"]))
+ word_nums += res["word_num"]
+
+ if fout:
+ fout.write(
+ f"{res['wav_path']}\t{res['wer']}\t{res['truth']}\t"
+ f"{res['hypo']}\t{res['insertions']}\t{res['deletions']}\t"
+ f"{res['substitutions']}\n"
+ )
+ lang_name = res["lang_name"]
+
+ # Per language stats
+ if lang_name not in lang_stats:
+ lang_stats[lang_name] = {
+ "inses": [],
+ "deles": [],
+ "subses": [],
+ "word_nums": 0,
+ }
+ lang_stats[lang_name]["inses"].append(float(res["insertions"]))
+ lang_stats[lang_name]["deles"].append(float(res["deletions"]))
+ lang_stats[lang_name]["subses"].append(float(res["substitutions"]))
+ lang_stats[lang_name]["word_nums"] += res["word_num"]
+
+ print("-" * 50)
+ # Log per-language stats
+ per_lang_wers = []
+ for lang in sorted(lang_stats.keys()):
+ stats = lang_stats[lang]
+ if stats["word_nums"] > 0:
+ lang_wer = log_metrics(
+ fout,
+ f"[{lang}]",
+ stats["inses"],
+ stats["deles"],
+ stats["subses"],
+ stats["word_nums"],
+ ndigits=3,
+ )
+ per_lang_wers.append(lang_wer)
+ print("-" * 50)
+
+ # Log Macro-average WER
+ if len(per_lang_wers) > 1:
+ macro_wer = np.mean(per_lang_wers)
+ logging.info(
+ f"Macro-average WER over {len(per_lang_wers)} languages: {macro_wer:.2f}%"
+ )
+ if fout:
+ fout.write(
+ f"Macro-average WER over {len(per_lang_wers)} languages: {macro_wer:.2f}%\n"
+ )
+
+ # Log overall stats
+ if word_nums > 0:
+ log_metrics(fout, "Overall", inses, deles, subses, word_nums)
+
+ if fout:
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/wer/norm_config_module.py b/omnivoice/eval/wer/norm_config_module.py
new file mode 100644
index 00000000..d2df2e52
--- /dev/null
+++ b/omnivoice/eval/wer/norm_config_module.py
@@ -0,0 +1,291 @@
+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the BSD-style license found in the
+# LICENSE file in the root directory of this source tree.
+
+"""
+This module defines the normalization configuration for WER evaluation.
+Copied from https://github.com/facebookresearch/omnilingual-asr/blob/81f51e224ce9e74b02cc2a3eaf21b2d91d743455/workflows/dataprep/norm_config_module.py
+"""
+
+# type: ignore
+import os
+import re
+
+colon = ":"
+comma = ","
+exclamation_mark = "!"
+period = re.escape(".")
+question_mark = re.escape("?")
+semicolon = ";"
+
+left_curly_bracket = "{"
+right_curly_bracket = "}"
+quotation_mark = '"'
+
+basic_punc = (
+ period
+ + question_mark
+ + comma
+ + colon
+ + exclamation_mark
+ + left_curly_bracket
+ + right_curly_bracket
+)
+
+# General punc unicode block (0x2000-0x206F)
+zero_width_space = r"\u200B"
+zero_width_nonjoiner = r"\u200C"
+left_to_right_mark = r"\u200E"
+right_to_left_mark = r"\u200F"
+left_to_right_embedding = r"\u202A"
+pop_directional_formatting = r"\u202C"
+
+# Here are some commonly ill-typed versions of apostrophe
+right_single_quotation_mark = r"\u2019"
+left_single_quotation_mark = r"\u2018"
+
+# Language specific definitions
+# Spanish
+inverted_exclamation_mark = r"\u00A1"
+inverted_question_mark = r"\u00BF"
+
+
+# Hindi
+hindi_danda = "\u0964"
+
+# Egyptian Arabic
+# arabic_percent = r"\u066A"
+arabic_comma = r"\u060C"
+arabic_question_mark = r"\u061F"
+arabic_semicolon = r"\u061B"
+arabic_diacritics = r"\u064B-\u0652"
+
+
+arabic_subscript_alef_and_inverted_damma = r"\u0656-\u0657"
+
+
+# Chinese
+full_stop = r"\u3002"
+full_comma = r"\uFF0C"
+full_exclamation_mark = r"\uFF01"
+full_question_mark = r"\uFF1F"
+full_semicolon = r"\uFF1B"
+full_colon = r"\uFF1A"
+full_parentheses = r"\uFF08\uFF09"
+quotation_mark_horizontal = r"\u300C-\u300F"
+quotation_mark_vertical = r"\uFF41-\uFF44"
+title_marks = r"\u3008-\u300B"
+wavy_low_line = r"\uFE4F"
+ellipsis = r"\u22EF"
+enumeration_comma = r"\u3001"
+hyphenation_point = r"\u2027"
+forward_slash = r"\uFF0F"
+wavy_dash = r"\uFF5E"
+box_drawings_light_horizontal = r"\u2500"
+fullwidth_low_line = r"\uFF3F"
+chinese_punc = (
+ full_stop
+ + full_comma
+ + full_exclamation_mark
+ + full_question_mark
+ + full_semicolon
+ + full_colon
+ + full_parentheses
+ + quotation_mark_horizontal
+ + quotation_mark_vertical
+ + title_marks
+ + wavy_low_line
+ + ellipsis
+ + enumeration_comma
+ + hyphenation_point
+ + forward_slash
+ + wavy_dash
+ + box_drawings_light_horizontal
+ + fullwidth_low_line
+)
+
+# Armenian
+armenian_apostrophe = r"\u055A"
+emphasis_mark = r"\u055B"
+exclamation_mark = r"\u055C"
+armenian_comma = r"\u055D"
+armenian_question_mark = r"\u055E"
+abbreviation_mark = r"\u055F"
+armenian_full_stop = r"\u0589"
+armenian_punc = (
+ armenian_apostrophe
+ + emphasis_mark
+ + exclamation_mark
+ + armenian_comma
+ + armenian_question_mark
+ + abbreviation_mark
+ + armenian_full_stop
+)
+
+lesser_than_symbol = r"<"
+greater_than_symbol = r">"
+
+lesser_than_sign = r"\u003c"
+greater_than_sign = r"\u003e"
+
+nbsp_written_form = r" "
+
+# Quotation marks
+left_double_quotes = r"\u201c"
+right_double_quotes = r"\u201d"
+left_double_angle = r"\u00ab"
+right_double_angle = r"\u00bb"
+left_single_angle = r"\u2039"
+right_single_angle = r"\u203a"
+low_double_quotes = r"\u201e"
+low_single_quotes = r"\u201a"
+high_double_quotes = r"\u201f"
+high_single_quotes = r"\u201b"
+
+all_punct_quotes = (
+ left_double_quotes
+ + right_double_quotes
+ + left_double_angle
+ + right_double_angle
+ + left_single_angle
+ + right_single_angle
+ + low_double_quotes
+ + low_single_quotes
+ + high_double_quotes
+ + high_single_quotes
+ + right_single_quotation_mark
+ + left_single_quotation_mark
+)
+mapping_quotes = (
+ "["
+ + high_single_quotes
+ + right_single_quotation_mark
+ + left_single_quotation_mark
+ + "]"
+)
+
+
+# Digits
+
+english_digits = r"\u0030-\u0039"
+bengali_digits = r"\u09e6-\u09ef"
+khmer_digits = r"\u17e0-\u17e9"
+devanagari_digits = r"\u0966-\u096f"
+oriya_digits = r"\u0b66-\u0b6f"
+extended_arabic_indic_digits = r"\u06f0-\u06f9"
+kayah_li_digits = r"\ua900-\ua909"
+fullwidth_digits = r"\uff10-\uff19"
+malayam_digits = r"\u0d66-\u0d6f"
+myanmar_digits = r"\u1040-\u1049"
+roman_numeral = r"\u2170-\u2179"
+nominal_digit_shapes = r"\u206f"
+
+# Load punctuations
+with open(f"{os.path.dirname(__file__)}/punctuations.lst", "r") as punc_f:
+ punc_list = [
+ line
+ for line in punc_f.readlines()
+ if line.strip() and not line.strip().startswith("#")
+ ]
+
+punct_pattern = r""
+for punc in punc_list:
+ # the first character in the tab separated line is the punc to be removed
+ punct_pattern += re.escape(punc.split("\t")[0])
+
+shared_digits = (
+ english_digits
+ + bengali_digits
+ + khmer_digits
+ + devanagari_digits
+ + oriya_digits
+ + extended_arabic_indic_digits
+ + kayah_li_digits
+ + fullwidth_digits
+ + malayam_digits
+ + myanmar_digits
+ + roman_numeral
+ + nominal_digit_shapes
+)
+
+shared_punc_list = (
+ basic_punc
+ + all_punct_quotes
+ + greater_than_sign
+ + lesser_than_sign
+ + inverted_question_mark
+ + full_stop
+ + semicolon
+ + armenian_punc
+ + inverted_exclamation_mark
+ + arabic_comma
+ + enumeration_comma
+ + hindi_danda
+ + quotation_mark
+ + arabic_semicolon
+ + arabic_question_mark
+ + chinese_punc
+ + punct_pattern
+)
+
+shared_mappping = {
+ lesser_than_symbol: "",
+ greater_than_symbol: "",
+ nbsp_written_form: "",
+ r"(\S+)" + mapping_quotes + r"(\S+)": r"\1'\2",
+}
+
+shared_deletion_list = (
+ left_to_right_mark
+ + zero_width_nonjoiner
+ + arabic_subscript_alef_and_inverted_damma
+ + zero_width_space
+ + arabic_diacritics
+ + pop_directional_formatting
+ + right_to_left_mark
+ + left_to_right_embedding
+)
+
+norm_config = {
+ "*": {
+ "lower_case": True,
+ "punc_set": shared_punc_list,
+ "del_set": shared_deletion_list,
+ "mapping": shared_mappping,
+ "digit_set": shared_digits,
+ "unicode_norm": "NFKC",
+ "rm_diacritics": False,
+ }
+}
+
+# =============== Mongolian ===============#
+
+norm_config["mon"] = norm_config["*"].copy()
+# add soft hyphen to punc list to match with fleurs
+norm_config["mon"]["del_set"] += r"\u00AD"
+
+norm_config["khk"] = norm_config["mon"].copy()
+
+# =============== Hebrew ===============#
+
+norm_config["heb"] = norm_config["*"].copy()
+# add "HEBREW POINT" symbols to match with fleurs
+norm_config["heb"]["del_set"] += r"\u05B0-\u05BF\u05C0-\u05CF"
+
+# =============== Thai ===============#
+
+norm_config["tha"] = norm_config["*"].copy()
+# add "Zero width joiner" symbols to match with fleurs
+norm_config["tha"]["punc_set"] += r"\u200D"
+
+# =============== Arabic ===============#
+norm_config["ara"] = norm_config["*"].copy()
+norm_config["ara"]["mapping"]["ٱ"] = "ا"
+norm_config["arb"] = norm_config["ara"].copy()
+
+# =============== Javanese ===============#
+norm_config["jav"] = norm_config["*"].copy()
+norm_config["jav"]["rm_diacritics"] = True
diff --git a/omnivoice/eval/wer/punctuations.lst b/omnivoice/eval/wer/punctuations.lst
new file mode 100644
index 00000000..f002b355
--- /dev/null
+++ b/omnivoice/eval/wer/punctuations.lst
@@ -0,0 +1,188 @@
+ 7355 INVALID UNICODE 0x81
+ 5265 INVALID UNICODE 0x90
+ 75 INVALID UNICODE 0x8
+ 31 INVALID UNICODE 0x8d
+ 3 INVALID UNICODE 0x94
+ 2 INVALID UNICODE 0x8f
+ 2 INVALID UNICODE 0x1a
+ 1 INVALID UNICODE 0x9d
+ 1 INVALID UNICODE 0x93
+ 1 INVALID UNICODE 0x92
+ 8647 INVALID UNICODE 0xe295
+ 6650 INVALID UNICODE 0xf21d
+ 6234 INVALID UNICODE 0xf62d
+ 4815 INVALID UNICODE 0xf173
+ 4789 INVALID UNICODE 0xe514
+ 4409 INVALID UNICODE 0xe293
+ 3881 INVALID UNICODE 0xf523
+ 3788 INVALID UNICODE 0xe233
+ 2448 INVALID UNICODE 0xf50f
+ 2177 INVALID UNICODE 0xe232
+ 1955 INVALID UNICODE 0xea7b
+ 1926 INVALID UNICODE 0xf172
+ 973 INVALID UNICODE 0xe290
+ 972 INVALID UNICODE 0xf519
+ 661 INVALID UNICODE 0xe292
+ 591 INVALID UNICODE 0xe328
+ 509 INVALID UNICODE 0xe2fa
+ 458 INVALID UNICODE 0xe234
+ 446 INVALID UNICODE 0xe043
+ 419 INVALID UNICODE 0xe040
+ 399 INVALID UNICODE 0xe2fb
+ 387 INVALID UNICODE 0xe32b
+ 381 INVALID UNICODE 0xe236
+ 374 INVALID UNICODE 0xf511
+ 314 INVALID UNICODE 0xe517
+ 296 INVALID UNICODE 0xe2fe
+ 293 INVALID UNICODE 0xe492
+ 291 INVALID UNICODE 0xf52d
+ 289 INVALID UNICODE 0xe2fc
+ 195 INVALID UNICODE 0xf521
+ 190 INVALID UNICODE 0xe516
+ 182 INVALID UNICODE 0xe041
+ 178 INVALID UNICODE 0xf529
+ 113 INVALID UNICODE 0xe2f9
+ 87 INVALID UNICODE 0xe2d9
+ 78 INVALID UNICODE 0xe32a
+ 76 INVALID UNICODE 0xe291
+ 74 INVALID UNICODE 0xe296
+ 66 INVALID UNICODE 0xe518
+ 52 INVALID UNICODE 0xe32c
+ 46 INVALID UNICODE 0xe2db
+ 41 INVALID UNICODE 0xe231
+ 34 INVALID UNICODE 0xf522
+ 33 INVALID UNICODE 0xf518
+ 32 INVALID UNICODE 0xf513
+ 27 INVALID UNICODE 0xe32d
+ 25 INVALID UNICODE 0xe32e
+ 23 INVALID UNICODE 0xe06b
+ 15 INVALID UNICODE 0xea01
+ 12 INVALID UNICODE 0xe294
+ 11 INVALID UNICODE 0xe203
+ 8 INVALID UNICODE 0xf218
+ 7 INVALID UNICODE 0xe070
+ 7 INVALID UNICODE 0xe013
+ 5 INVALID UNICODE 0xe2de
+ 4 INVALID UNICODE 0xe493
+ 3 INVALID UNICODE 0xf7e8
+ 3 INVALID UNICODE 0xf7d0
+ 3 INVALID UNICODE 0xe313
+ 2 INVALID UNICODE 0xe329
+ 2 INVALID UNICODE 0xe06d
+ 2 INVALID UNICODE 0xe003
+ 1 INVALID UNICODE 0xf50e
+ 1 INVALID UNICODE 0xf171
+ 1 INVALID UNICODE 0xe01d
+ 71 NOMINAL DIGIT SHAPES 0x206f
+ 3 WORD JOINER 0x2060
+― 126545 HORIZONTAL BAR 0x2015
+־ 1028 HEBREW PUNCTUATION MAQAF 0x5be
+) 98429 RIGHT PARENTHESIS 0x29
+] 27108 RIGHT SQUARE BRACKET 0x5d
+⌋ 1567 RIGHT FLOOR 0x230b
+〕 97 RIGHT TORTOISE SHELL BRACKET 0x3015
+】 36 RIGHT BLACK LENTICULAR BRACKET 0x3011
+﴾ 14 ORNATE LEFT PARENTHESIS 0xfd3e
+& 170517 AMPERSAND 0x26
+། 106330 TIBETAN MARK SHAD 0xf0d
+። 90203 ETHIOPIC FULL STOP 0x1362
+፥ 60484 ETHIOPIC COLON 0x1365
+༌ 60464 TIBETAN MARK DELIMITER TSHEG BSTAR 0xf0c
+။ 51567 MYANMAR SIGN SECTION 0x104b
+/ 46929 SOLIDUS 0x2f
+၊ 38042 MYANMAR SIGN LITTLE SECTION 0x104a
+· 37985 MIDDLE DOT 0xb7
+‸ 36310 CARET 0x2038
+* 34793 ASTERISK 0x2a
+۔ 32432 ARABIC FULL STOP 0x6d4
+፤ 31906 ETHIOPIC SEMICOLON 0x1364
+၏ 21519 MYANMAR SYMBOL GENITIVE 0x104f
+។ 20834 KHMER SIGN KHAN 0x17d4
+꓾ 15773 LISU PUNCTUATION COMMA 0xa4fe
+᙮ 13473 CANADIAN SYLLABICS FULL STOP 0x166e
+꤯ 12892 KAYAH LI SIGN SHYA 0xa92f
+⵰ 11478 TIFINAGH SEPARATOR MARK 0x2d70
+꓿ 11118 LISU PUNCTUATION FULL STOP 0xa4ff
+॥ 10763 DEVANAGARI DOUBLE DANDA 0x965
+؞ 10403 ARABIC TRIPLE DOT PUNCTUATION MARK 0x61e
+၍ 8936 MYANMAR SYMBOL COMPLETED 0x104d
+· 8431 GREEK ANO TELEIA 0x387
+† 7477 DAGGER 0x2020
+၌ 6632 MYANMAR SYMBOL LOCATIVE 0x104c
+፣ 5719 ETHIOPIC COMMA 0x1363
+៖ 5528 KHMER SIGN CAMNUC PII KUUH 0x17d6
+꤮ 4791 KAYAH LI SIGN CWI 0xa92e
+※ 3439 REFERENCE MARK 0x203b
+፦ 2727 ETHIOPIC PREFACE COLON 0x1366
+• 1749 BULLET 0x2022
+¶ 1507 PILCROW SIGN 0xb6
+၎ 1386 MYANMAR SYMBOL AFOREMENTIONED 0x104e
+﹖ 1224 SMALL QUESTION MARK 0xfe56
+; 975 GREEK QUESTION MARK 0x37e
+… 827 HORIZONTAL ELLIPSIS 0x2026
+% 617 PERCENT SIGN 0x25
+・ 468 KATAKANA MIDDLE DOT 0x30fb
+༎ 306 TIBETAN MARK NYIS SHAD 0xf0e
+‡ 140 DOUBLE DAGGER 0x2021
+# 137 NUMBER SIGN 0x23
+@ 125 COMMERCIAL AT 0x40
+፡ 121 ETHIOPIC WORDSPACE 0x1361
+៚ 55 KHMER SIGN KOOMUUT 0x17da
+៕ 49 KHMER SIGN BARIYOOSAN 0x17d5
+﹐ 10 SMALL COMMA 0xfe50
+༅ 6 TIBETAN MARK CLOSING YIG MGO SGAB MA 0xf05
+༄ 6 TIBETAN MARK INITIAL YIG MGO MDUN MA 0xf04
+. 2 FULLWIDTH FULL STOP 0xff0e
+﹗ 2 SMALL EXCLAMATION MARK 0xfe57
+﹕ 2 SMALL COLON 0xfe55
+‰ 2 PER MILLE SIGN 0x2030
+・ 1 HALFWIDTH KATAKANA MIDDLE DOT 0xff65
+( 98504 LEFT PARENTHESIS 0x28
+[ 27245 LEFT SQUARE BRACKET 0x5b
+⌊ 1567 LEFT FLOOR 0x230a
+〔 95 LEFT TORTOISE SHELL BRACKET 0x3014
+【 36 LEFT BLACK LENTICULAR BRACKET 0x3010
+﴿ 14 ORNATE RIGHT PARENTHESIS 0xfd3f
+_ 4851 LOW LINE 0x5f
+$ 72 DOLLAR SIGN 0x24
+€ 14 EURO SIGN 0x20ac
+£ 2 POUND SIGN 0xa3
+~ 27462 TILDE 0x7e
+= 11450 EQUALS SIGN 0x3d
+| 8430 VERTICAL LINE 0x7c
+− 3971 MINUS SIGN 0x2212
+≫ 1904 MUCH GREATER-THAN 0x226b
+≪ 1903 MUCH LESS-THAN 0x226a
++ 1450 PLUS SIGN 0x2b
+< 345 FULLWIDTH LESS-THAN SIGN 0xff1c
+> 344 FULLWIDTH GREATER-THAN SIGN 0xff1e
+¬ 5 NOT SIGN 0xac
+× 4 MULTIPLICATION SIGN 0xd7
+→ 2 RIGHTWARDS ARROW 0x2192
+᙭ 537 CANADIAN SYLLABICS CHI SIGN 0x166d
+° 499 DEGREE SIGN 0xb0
+႟ 421 MYANMAR SYMBOL SHAN EXCLAMATION 0x109f
+� 192 REPLACEMENT CHARACTER 0xfffd
+⌟ 54 BOTTOM RIGHT CORNER 0x231f
+⌞ 54 BOTTOM LEFT CORNER 0x231e
+© 2 COPYRIGHT SIGN 0xa9
+ 40 NARROW NO-BREAK SPACE 0x202f
+ 1 SIX-PER-EM SPACE 0x2006
+˜ 40261 SMALL TILDE 0x2dc
+^ 6469 CIRCUMFLEX ACCENT 0x5e
+¯ 20 MACRON 0xaf
+ˇ 191442 CARON 0x2c7
+ⁿ 38144 SUPERSCRIPT LATIN SMALL LETTER N 0x207f
+ـ 9440 ARABIC TATWEEL 0x640
+ๆ 6766 THAI CHARACTER MAIYAMOK 0xe46
+ៗ 3310 KHMER SIGN LEK TOO 0x17d7
+々 678 IDEOGRAPHIC ITERATION MARK 0x3005
+ໆ 430 LAO KO LA 0xec6
+ー 319 KATAKANA-HIRAGANA PROLONGED SOUND MARK 0x30fc
+ⁱ 137 SUPERSCRIPT LATIN SMALL LETTER I 0x2071
+৷ 11056 BENGALI CURRENCY NUMERATOR FOUR 0x9f7
+⅓ 26 VULGAR FRACTION ONE THIRD 0x2153
+½ 26 VULGAR FRACTION ONE HALF 0xbd
+¼ 4 VULGAR FRACTION ONE QUARTER 0xbc
+⅟ 1 FRACTION NUMERATOR ONE 0x215f
+⁄ 57 FRACTION SLASH 0x2044
diff --git a/omnivoice/eval/wer/seedtts.py b/omnivoice/eval/wer/seedtts.py
new file mode 100755
index 00000000..2fcb04f5
--- /dev/null
+++ b/omnivoice/eval/wer/seedtts.py
@@ -0,0 +1,413 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Computes word error rate (WER) with Whisper-large-v3 for English and
+Paraformer for Chinese. Intended to evaluate WERs on Seed-TTS test sets.
+"""
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import string
+import traceback
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from pathlib import Path
+
+import numpy as np
+import torch
+import zhconv
+from tqdm import tqdm
+from zhon.hanzi import punctuation
+
+from omnivoice.eval.utils import load_waveform
+from omnivoice.eval.wer.common import process_one
+from omnivoice.utils.data_utils import read_test_list
+
+# --- Global variables for worker processes ---
+worker_pipe = None
+worker_device = None
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Computes WER with Whisper/Paraformer.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing speech files.",
+ )
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where WER information will be saved. "
+ "If not provided, results are only printed to console.",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of evaluation models repository. "
+ "Download from https://huggingface.co/k2-fsa/TTS_eval_models. "
+ "This script expects 'tts_eval_models/wer/whisper-large-v3/' for English "
+ "and 'tts_eval_models/wer/paraformer-zh/' for Chinese within this directory.",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ default="test.jsonl",
+ help="path of the JSONL test list. Each line is a JSON object "
+ "with fields: id, text, ref_audio, ref_text, language_id, language_name.",
+ )
+ parser.add_argument(
+ "--lang",
+ type=str,
+ choices=["zh", "en"],
+ required=True,
+ help="Language of the audio and transcripts for "
+ "decoding ('zh' for Chinese or 'en' for English).",
+ )
+ parser.add_argument(
+ "--batch-size",
+ type=int,
+ default=16,
+ help="Batch size for decoding with the Hugging Face pipeline.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu", type=int, default=1, help="Number of workers per GPU."
+ )
+ return parser
+
+
+def load_whisper_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/whisper-large-v3/")
+ if not os.path.exists(model_path):
+ logging.error(f"Whisper model not found at {model_path}.")
+ return None
+
+ logging.debug(f"Loading Whisper model on {device}...")
+
+ import transformers
+
+ # Suppress transformers logging
+ transformers.logging.set_verbosity_error()
+
+ pipe = transformers.pipeline(
+ "automatic-speech-recognition",
+ model=model_path,
+ dtype=torch.float16 if "cuda" in str(device) else torch.float32,
+ device=device,
+ )
+ return pipe
+
+
+def load_paraformer_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/paraformer-zh/")
+ if not os.path.exists(model_path):
+ logging.error(f"Paraformer model not found at {model_path}.")
+ return None
+
+ logging.debug(f"Loading Paraformer model on {device}...")
+
+ previous_level = logging.root.manager.disable
+ logging.disable(logging.CRITICAL)
+
+ try:
+ from funasr import AutoModel
+
+ # FunASR AutoModel accepts "cuda:0" string or torch.device
+ model = AutoModel(
+ model=model_path,
+ device=str(device),
+ disable_update=True,
+ disable_pbar=True,
+ verbose=False,
+ )
+ finally:
+ logging.disable(previous_level)
+
+ return model
+
+
+def post_process(text: str, lang: str) -> str:
+ """
+ Cleans and normalizes text for WER calculation.
+ Args:
+ text (str): The input text to be processed.
+ lang (str): The language of the input text.
+
+ Returns:
+ str: The cleaned and normalized text.
+ """
+ punctuation_all = punctuation + string.punctuation
+ for x in punctuation_all:
+ if x == "'":
+ continue
+ text = text.replace(x, "")
+
+ text = text.replace(" ", " ")
+
+ if lang == "zh":
+ text = " ".join([x for x in text])
+ elif lang == "en":
+ text = text.lower()
+ else:
+ raise NotImplementedError
+ return text
+
+
+def process_init(rank_queue, model_dir, lang):
+ """
+ Initializer for each worker process.
+ Loads model onto a specific GPU, once per process.
+ """
+ global worker_pipe, worker_device
+
+ torch.set_num_threads(2)
+
+ try:
+ rank = rank_queue.get(timeout=10)
+ except Exception:
+ raise RuntimeError("Failed to get GPU rank from queue.")
+
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ worker_device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+ try:
+ if lang == "en":
+ worker_pipe = load_whisper_model(model_dir, worker_device)
+ elif lang == "zh":
+ worker_pipe = load_paraformer_model(model_dir, worker_device)
+ if worker_pipe is None:
+ raise RuntimeError("Model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load model on {worker_device}: {e}")
+ raise e
+
+
+def run_eval_worker(data_chunk, lang, batch_size):
+ """
+ Worker function to process a chunk of data.
+ Uses the global worker_pipe initialized by process_init.
+ """
+ global worker_pipe
+ if worker_pipe is None:
+ logging.error("Worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ if lang == "en":
+ # Load waveforms as arrays, truncating to 30s
+ dataset = [
+ {
+ "array": load_waveform(
+ item["wav_path"], sample_rate=16000, return_numpy=True
+ )[: 16000 * 30],
+ "sampling_rate": 16000,
+ }
+ for item in data_chunk
+ ]
+ generate_kwargs = {"language": "english", "task": "transcribe"}
+
+ iterator = worker_pipe(
+ dataset, generate_kwargs=generate_kwargs, batch_size=batch_size
+ )
+
+ for i, out in enumerate(iterator):
+ hypothesis = out["text"].strip()
+ ref_item = data_chunk[i]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+
+ m = process_one(hypothesis, truth, post_process, lang)
+ m["wav_path"] = wav_path
+ metrics_buffer.append(m)
+
+ elif lang == "zh":
+ wav_paths = [item["wav_path"] for item in data_chunk]
+
+ for i in range(0, len(wav_paths), batch_size):
+ batch_paths = wav_paths[i : i + batch_size]
+ res_batch = worker_pipe.generate(
+ input=batch_paths, batch_size=batch_size, disable_pbar=True
+ )
+
+ for j, res in enumerate(res_batch):
+ hypothesis = zhconv.convert(res["text"], "zh-cn")
+ ref_item = data_chunk[i + j]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+
+ m = process_one(hypothesis, truth, post_process, lang)
+ m["wav_path"] = wav_path
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(
+ f"Worker failed on chunk (Lang: {lang}):\n{traceback.format_exc()}"
+ )
+ return []
+
+ return metrics_buffer
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+ level=logging.INFO,
+ force=True,
+ )
+
+ logging.info(f"Calculating WER for {args.wav_path}")
+
+ # 1. Prepare Data
+ logging.info("Reading test list...")
+ data_list = []
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ wav_path = str(Path(args.wav_path) / f"{s['id']}.{args.extension}")
+ if not os.path.exists(wav_path):
+ logging.warning(f"File missing: {wav_path}")
+ continue
+ data_list.append({"wav_path": wav_path, "truth_text": s["text"]})
+ total_files = len(data_list)
+ logging.info(f"Total files: {total_files}.")
+
+ # 2. Worker config
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_workers = num_gpus * args.nj_per_gpu
+
+ mp.set_start_method("spawn", force=True)
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ rank_queue.put(rank)
+
+ # 3. Scheduling: Split data into chunks for better load balancing
+ chunk_size = max(1, args.batch_size)
+ tasks = []
+ for i in range(0, total_files, chunk_size):
+ tasks.append(data_list[i : i + chunk_size])
+
+ logging.info(
+ f"Split data into {len(tasks)} chunks (size ~{chunk_size}). "
+ f"Spawning {total_workers} workers."
+ )
+
+ # 4. Execution
+ results = []
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init,
+ initargs=(rank_queue, args.model_dir, args.lang),
+ ) as executor:
+
+ futures = []
+ for chunk in tasks:
+ futures.append(
+ executor.submit(run_eval_worker, chunk, args.lang, args.batch_size)
+ )
+
+ # Unified progress bar
+ with tqdm(total=total_files, desc="Eval Progress", dynamic_ncols=True) as pbar:
+ for future in as_completed(futures):
+ try:
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+ except Exception as e:
+ logging.error(f"Task failed: {e}")
+
+ wers, inses, deles, subses = [], [], [], []
+ word_nums = 0
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ fout = open(args.decode_path, "w", encoding="utf8")
+ logging.info(f"Saving detailed WER results to: {args.decode_path}")
+ fout.write(
+ "Name\tWER\tTruth\tHypothesis\tInsertions\tDeletions\tSubstitutions\n"
+ )
+
+ for res in results:
+ wers.append(float(res["wer"]))
+ inses.append(float(res["insertions"]))
+ deles.append(float(res["deletions"]))
+ subses.append(float(res["substitutions"]))
+ word_nums += res["word_num"]
+
+ if fout:
+ fout.write(
+ f"{res['wav_path']}\t{res['wer']}\t{res['truth']}\t"
+ f"{res['hypo']}\t{res['insertions']}\t{res['deletions']}\t"
+ f"{res['substitutions']}\n"
+ )
+
+ wer_avg = round(np.mean(wers) * 100, 2) if wers else float("nan")
+ wer_weighted = (
+ round(
+ (np.sum(subses) + np.sum(deles) + np.sum(inses)) / word_nums * 100, 2
+ )
+ if word_nums > 0
+ else float("nan")
+ )
+
+ inse_sum = np.sum(inses)
+ dele_sum = np.sum(deles)
+ subs_sum = np.sum(subses)
+
+ print("-" * 50)
+ logging.info(f"Processed {len(results)}/{total_files} files.")
+ seedtts_wer_info = f"Seed-TTS WER (Avg of WERs): {wer_avg}%"
+ wer_info = f"WER (Weighted): {wer_weighted}%"
+ detailed_info = (
+ f"Errors: {inse_sum} ins, {dele_sum} del, {subs_sum} sub / {word_nums} words"
+ )
+ logging.info(seedtts_wer_info)
+ logging.info(wer_info)
+ logging.info(detailed_info)
+ print("-" * 50)
+
+ if fout:
+ fout.write(seedtts_wer_info + "\n" + wer_info + "\n" + detailed_info + "\n")
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/wer/sensevoice.py b/omnivoice/eval/wer/sensevoice.py
new file mode 100644
index 00000000..8490c22e
--- /dev/null
+++ b/omnivoice/eval/wer/sensevoice.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Computes Character Error Rate (CER) for Cantonese (yue) using SenseVoiceSmall.
+"""
+
+import argparse
+import logging
+import multiprocessing as mp
+import os
+import re
+import traceback
+from concurrent.futures import ProcessPoolExecutor, as_completed
+from pathlib import Path
+
+import cn2an
+import torch
+import zhconv
+from tqdm import tqdm
+
+from omnivoice.eval.wer.common import log_metrics, process_one
+from omnivoice.eval.wer.text_norm_omni import text_normalize
+from omnivoice.utils.data_utils import read_test_list
+
+# --- Global variables for worker processes ---
+worker_sensevoice = None
+worker_device = None
+
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Computes CER for Cantonese using SenseVoiceSmall.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+
+ parser.add_argument(
+ "--wav-path",
+ type=str,
+ required=True,
+ help="Path to the directory containing speech files.",
+ )
+
+ parser.add_argument(
+ "--extension",
+ type=str,
+ default="wav",
+ help="Extension of the speech files. Default: wav",
+ )
+
+ parser.add_argument(
+ "--decode-path",
+ type=str,
+ default=None,
+ help="Path to the output file where CER information will be saved. ",
+ )
+ parser.add_argument(
+ "--model-dir",
+ type=str,
+ required=True,
+ help="Local path of evaluation models repository. ",
+ )
+ parser.add_argument(
+ "--test-list",
+ type=str,
+ default="test.jsonl",
+ help="path of the JSONL test list.",
+ )
+ parser.add_argument(
+ "--batch-size",
+ type=int,
+ default=16,
+ help="Batch size for decoding.",
+ )
+ parser.add_argument(
+ "--nj-per-gpu", type=int, default=1, help="Number of workers per GPU."
+ )
+ parser.add_argument(
+ "--chunk-size",
+ type=int,
+ default=10,
+ help="Number of samples per task chunk sent to workers.",
+ )
+ return parser
+
+
+def load_sensevoice_model(model_dir, device):
+ model_path = os.path.join(model_dir, "wer/SenseVoiceSmall")
+ if not os.path.exists(model_path):
+ # Fallback if specific sensevoice spelling isn't found
+ logging.warning(
+ f"SenseVoiceSmall not found at {model_path}. "
+ f"Please ensure it is present in eval models."
+ )
+
+ logging.info(f"Loading SenseVoice model on {device}...")
+
+ previous_level = logging.root.manager.disable
+ logging.disable(logging.CRITICAL)
+
+ try:
+ from funasr import AutoModel
+
+ model = AutoModel(
+ model="iic/SenseVoiceSmall",
+ device=str(device),
+ disable_update=True,
+ disable_pbar=True,
+ verbose=False,
+ )
+ finally:
+ logging.disable(previous_level)
+
+ return model
+
+
+def _worker_setup(rank_queue):
+ global worker_device
+
+ torch.set_num_threads(2)
+
+ try:
+ rank = rank_queue.get(timeout=10)
+ except Exception:
+ raise RuntimeError("Failed to get GPU rank from queue.")
+
+ assert torch.cuda.is_available(), "CUDA is required but not available."
+ worker_device = torch.device(f"cuda:{rank}")
+ torch.cuda.set_device(rank)
+
+ logging.info(f"Initializing worker on device: {worker_device}")
+
+
+def process_init_sensevoice(rank_queue, model_dir):
+ global worker_sensevoice
+
+ _worker_setup(rank_queue)
+
+ try:
+ worker_sensevoice = load_sensevoice_model(model_dir, worker_device)
+ if worker_sensevoice is None:
+ raise RuntimeError("SenseVoice model loading failed.")
+ except Exception as e:
+ logging.critical(f"Failed to load SenseVoice model on {worker_device}: {e}")
+ raise e
+
+
+def post_process(text: str, lang: str) -> str:
+ """
+ Cleans and normalizes text for calculation.
+ """
+ assert lang == "yue", "this script is designed for Cantonese (yue) evaluation only."
+ text = text_normalize(
+ text,
+ iso_code="yue",
+ lower_case=True,
+ remove_numbers=False,
+ remove_brackets=False,
+ )
+
+ text = zhconv.convert(text, "zh-cn")
+
+ text = cn2an.transform(text, "an2cn")
+
+ text = text.replace(" ", "")
+ text = " ".join([x for x in text])
+ text = text.lower()
+ return text.strip()
+
+
+def run_eval_worker_sensevoice(data_chunk, batch_size):
+ global worker_sensevoice
+ if worker_sensevoice is None:
+ logging.error("SenseVoice worker pipeline is not initialized!")
+ return []
+
+ metrics_buffer = []
+ try:
+ wav_paths = [item["wav_path"] for item in data_chunk]
+
+ for i in range(0, len(wav_paths), batch_size):
+ batch_paths = wav_paths[i : i + batch_size]
+
+ # SenseVoice generate call, target lang mapped to yue
+ res_batch = worker_sensevoice.generate(
+ input=batch_paths,
+ batch_size=batch_size,
+ language="yue",
+ use_itn=False,
+ disable_pbar=True,
+ )
+
+ for j, res in enumerate(res_batch):
+ hypothesis = res["text"]
+ # SenseVoice may format output with language tags,
+ # cleaning basic tags if any
+ hypothesis = re.sub(r"<\|[^|]*\|>", "", hypothesis).strip()
+
+ ref_item = data_chunk[i + j]
+ truth = ref_item["truth_text"]
+ wav_path = ref_item["wav_path"]
+ lang_name = ref_item.get("lang_name")
+
+ m = process_one(hypothesis, truth, post_process, "yue")
+ m["wav_path"] = wav_path
+ m["lang_name"] = lang_name
+ metrics_buffer.append(m)
+
+ except Exception:
+ logging.error(f"SenseVoice worker failed on chunk:\n{traceback.format_exc()}")
+ return []
+
+ return metrics_buffer
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s",
+ level=logging.INFO,
+ force=True,
+ )
+
+ logging.info("Reading test list and filtering for Cantonese (yue)...")
+ yue_items = []
+ wav_root = Path(args.wav_path)
+
+ samples = read_test_list(args.test_list)
+ for s in samples:
+ lang_id = s.get("language_id", "")
+ if lang_id != "yue":
+ continue
+
+ wav_path = str(wav_root / f"{s['id']}.{args.extension}")
+ if not os.path.exists(wav_path):
+ logging.warning(f"File missing: {wav_path}")
+ continue
+
+ yue_items.append(
+ {
+ "wav_path": wav_path,
+ "truth_text": s["text"],
+ "lang_id": "yue",
+ "lang_name": s.get("language_name", "Cantonese"),
+ }
+ )
+
+ logging.info(f"Total Cantonese files found: {len(yue_items)}.")
+ if len(yue_items) == 0:
+ logging.warning("No files to evaluate. Exiting.")
+ return
+
+ num_gpus = torch.cuda.device_count()
+ assert num_gpus > 0, "No GPU found. GPU is required."
+ total_workers = num_gpus * args.nj_per_gpu
+
+ mp.set_start_method("spawn", force=True)
+ manager = mp.Manager()
+
+ chunk_size = args.chunk_size
+ tasks = []
+ for i in range(0, len(yue_items), chunk_size):
+ tasks.append(yue_items[i : i + chunk_size])
+
+ results = []
+ rank_queue = manager.Queue()
+ for _ in range(args.nj_per_gpu):
+ for rank in range(num_gpus):
+ rank_queue.put(rank)
+
+ with ProcessPoolExecutor(
+ max_workers=total_workers,
+ initializer=process_init_sensevoice,
+ initargs=(rank_queue, args.model_dir),
+ ) as executor:
+
+ futures = []
+ for chunk in tasks:
+ futures.append(
+ executor.submit(run_eval_worker_sensevoice, chunk, args.batch_size)
+ )
+
+ with tqdm(
+ total=len(yue_items),
+ desc="SenseVoice Eval (Cantonese)",
+ dynamic_ncols=True,
+ ) as pbar:
+ for future in as_completed(futures):
+ try:
+ chunk_metrics = future.result()
+ results.extend(chunk_metrics)
+ pbar.update(len(chunk_metrics))
+ except Exception as e:
+ logging.error(f"Task failed: {e}")
+
+ # Metrics Aggregation
+ inses, deles, subses = [], [], []
+ word_nums = 0
+
+ fout = None
+ if args.decode_path:
+ os.makedirs(os.path.dirname(args.decode_path), exist_ok=True)
+ logging.info(f"Saving detailed CER results to: {args.decode_path}")
+ fout = open(args.decode_path, "w", encoding="utf-8")
+
+ for res in results:
+ inses.append(float(res["insertions"]))
+ deles.append(float(res["deletions"]))
+ subses.append(float(res["substitutions"]))
+ word_nums += res["word_num"]
+
+ if fout:
+ fout.write(
+ f"{res['wav_path']}\t{res['wer']}\t{res['truth']}\t"
+ f"{res['hypo']}\t{res['insertions']}\t{res['deletions']}\t"
+ f"{res['substitutions']}\n"
+ )
+
+ print("-" * 50)
+ if word_nums > 0:
+ log_metrics(fout, "[yue] Cantonese", inses, deles, subses, word_nums)
+
+ if fout:
+ fout.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/eval/wer/text_norm_omni.py b/omnivoice/eval/wer/text_norm_omni.py
new file mode 100644
index 00000000..fc435e8e
--- /dev/null
+++ b/omnivoice/eval/wer/text_norm_omni.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the BSD-style license found in the
+# LICENSE file in the root directory of this source tree.
+
+"""
+This module contains the text normalization function for WER evaluation.
+Copied from https://github.com/facebookresearch/omnilingual-asr/blob/81f51e224ce9e74b02cc2a3eaf21b2d91d743455/workflows/dataprep/text_tools.py
+"""
+
+import re
+import unicodedata
+
+from unidecode import unidecode
+
+import omnivoice.eval.wer.norm_config_module as norm_config_module
+
+norm_config = norm_config_module.norm_config # type: ignore
+
+
+def text_normalize(
+ text, iso_code, lower_case=True, remove_numbers=True, remove_brackets=False
+):
+ """Given a text, normalize it by changing to lower case, removing punctuations, removing words that only contain digits and removing extra spaces
+
+ Args:
+ text : The string to be normalized
+ iso_code :
+ remove_numbers : Boolean flag to specify if words containing only digits should be removed
+
+ Returns:
+ normalized_text : the string after all normalization
+
+ """
+
+ config = norm_config.get(iso_code, norm_config["*"])
+
+ for field in [
+ "lower_case",
+ "punc_set",
+ "del_set",
+ "mapping",
+ "digit_set",
+ "unicode_norm",
+ ]:
+ if field not in config:
+ config[field] = norm_config["*"][field]
+
+ text = unicodedata.normalize(config["unicode_norm"], text)
+
+ # Convert to lower case
+
+ if config["lower_case"] and lower_case:
+ text = text.lower()
+
+ # brackets
+
+ # always text inside brackets with numbers in them. Usually corresponds to "(Sam 23:17)"
+ text = re.sub(r"\([^\)]*\d[^\)]*\)", " ", text)
+ if remove_brackets:
+ text = re.sub(r"\([^\)]*\)", " ", text)
+
+ # Apply mappings
+
+ for old, new in config["mapping"].items():
+ text = re.sub(old, new, text)
+
+ # Replace punctutations with space
+
+ punct_pattern = r"[" + config["punc_set"]
+
+ punct_pattern += "]"
+
+ normalized_text = re.sub(punct_pattern, " ", text)
+
+ # remove characters in delete list
+
+ delete_patten = r"[" + config["del_set"] + "]"
+
+ normalized_text = re.sub(delete_patten, "", normalized_text)
+
+ # Remove words containing only digits
+ # We check for 3 cases a)text starts with a number b) a number is present somewhere in the middle of the text c) the text ends with a number
+ # For each case we use lookaround regex pattern to see if the digit pattern in preceded and followed by whitespaces, only then we replace the numbers with space
+ # The lookaround enables overlapping pattern matches to be replaced
+
+ if remove_numbers:
+
+ digits_pattern = "[" + config["digit_set"]
+
+ digits_pattern += "]+"
+
+ complete_digit_pattern = (
+ r"^"
+ + digits_pattern
+ + r"(?=\s)|(?<=\s)"
+ + digits_pattern
+ + r"(?=\s)|(?<=\s)"
+ + digits_pattern
+ + "$"
+ )
+
+ normalized_text = re.sub(complete_digit_pattern, " ", normalized_text)
+
+ if config["rm_diacritics"]:
+ normalized_text = unidecode(normalized_text)
+
+ # Remove extra spaces
+ normalized_text = re.sub(r"\s+", " ", normalized_text).strip()
+
+ return normalized_text
diff --git a/omnivoice/models/__init__.py b/omnivoice/models/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/models/omnivoice.py b/omnivoice/models/omnivoice.py
new file mode 100644
index 00000000..a3e8b276
--- /dev/null
+++ b/omnivoice/models/omnivoice.py
@@ -0,0 +1,1568 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Core OmniVoice model implementation.
+
+Defines the ``OmniVoice`` model class, generation config, and inference pipeline.
+This is the main entry point for both inference and training:
+
+- **Inference**: ``OmniVoice.from_pretrained()`` loads the model, then
+ ``model.generate()`` supports voice cloning, voice design, and auto voice.
+- **Training**: ``model.forward()`` computes the training loss; the model is
+ built and used by ``omnivoice.training.builder`` and ``omnivoice.training.trainer``.
+
+"""
+
+import difflib
+import logging
+import math
+import os
+import re
+from dataclasses import dataclass, fields
+from functools import partial
+from typing import Any, List, Optional, Union
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torchaudio
+from torch.nn.attention.flex_attention import create_block_mask
+from transformers import (
+ AutoFeatureExtractor,
+ AutoModel,
+ AutoTokenizer,
+ HiggsAudioV2TokenizerModel,
+ PretrainedConfig,
+ PreTrainedModel,
+)
+from transformers.modeling_outputs import ModelOutput
+from transformers.models.auto import CONFIG_MAPPING, AutoConfig
+
+from omnivoice.utils.audio import (
+ cross_fade_chunks,
+ fade_and_pad_audio,
+ load_audio,
+ remove_silence,
+ trim_long_audio,
+)
+from omnivoice.utils.duration import RuleDurationEstimator
+from omnivoice.utils.lang_map import LANG_IDS, LANG_NAMES
+from omnivoice.utils.text import add_punctuation, chunk_text_punctuation
+from omnivoice.utils.voice_design import (
+ _INSTRUCT_ALL_VALID,
+ _INSTRUCT_EN_TO_ZH,
+ _INSTRUCT_MUTUALLY_EXCLUSIVE,
+ _INSTRUCT_VALID_EN,
+ _INSTRUCT_VALID_ZH,
+ _INSTRUCT_ZH_TO_EN,
+ _ZH_RE,
+)
+
+logger = logging.getLogger(__name__)
+
+
+# ---------------------------------------------------------------------------
+# Dataclasses
+# ---------------------------------------------------------------------------
+
+
+@dataclass
+class VoiceClonePrompt:
+ ref_audio_tokens: torch.Tensor # (C, T)
+ ref_text: str
+ ref_rms: float
+
+
+@dataclass
+class OmniVoiceGenerationConfig:
+ num_step: int = 32
+ guidance_scale: float = 2.0
+ t_shift: float = 0.1
+ layer_penalty_factor: float = 5.0
+ position_temperature: float = 5.0
+ class_temperature: float = 0.0
+ denoise: bool = True
+ preprocess_prompt: bool = True
+ postprocess_output: bool = True
+ audio_chunk_duration: float = 15.0
+ audio_chunk_threshold: float = 30.0
+
+ @classmethod
+ def from_dict(cls, kwargs_dict):
+ valid_keys = {f.name for f in fields(cls)}
+ filtered = {k: v for k, v in kwargs_dict.items() if k in valid_keys}
+ return cls(**filtered)
+
+
+@dataclass
+class GenerationTask:
+ batch_size: int
+ texts: List[str]
+ target_lens: List[int]
+ langs: List[Optional[str]]
+ instructs: List[Optional[str]]
+ ref_texts: List[Optional[str]]
+ ref_audio_tokens: List[Optional[torch.Tensor]]
+ ref_rms: List[Optional[float]]
+ speed: Optional[List[float]] = None
+
+ def get_indices(self, config: OmniVoiceGenerationConfig, frame_rate: int):
+ threshold = int(config.audio_chunk_threshold * frame_rate)
+ short_idx = [i for i, l in enumerate(self.target_lens) if l <= threshold]
+ long_idx = [i for i, l in enumerate(self.target_lens) if l > threshold]
+ return short_idx, long_idx
+
+ def slice_task(self, indices: List[int]):
+ if not indices:
+ return None
+ return GenerationTask(
+ batch_size=len(indices),
+ texts=[self.texts[i] for i in indices],
+ target_lens=[self.target_lens[i] for i in indices],
+ langs=[self.langs[i] for i in indices],
+ instructs=[self.instructs[i] for i in indices],
+ ref_texts=[self.ref_texts[i] for i in indices],
+ ref_audio_tokens=[self.ref_audio_tokens[i] for i in indices],
+ ref_rms=[self.ref_rms[i] for i in indices],
+ speed=[self.speed[i] for i in indices] if self.speed else None,
+ )
+
+
+@dataclass
+class OmniVoiceModelOutput(ModelOutput):
+ loss: Optional[torch.Tensor] = None
+ logits: Optional[torch.Tensor] = None
+
+
+# ---------------------------------------------------------------------------
+# Config & Model
+# ---------------------------------------------------------------------------
+
+
+class OmniVoiceConfig(PretrainedConfig):
+ model_type = "omnivoice"
+ sub_configs = {"llm_config": AutoConfig}
+
+ def __init__(
+ self,
+ audio_vocab_size: int = 1025,
+ audio_mask_id: int = 1024,
+ num_audio_codebook: int = 8,
+ audio_codebook_weights: Optional[list[float]] = None,
+ llm_config: Optional[Union[dict, PretrainedConfig]] = None,
+ **kwargs,
+ ):
+
+ if isinstance(llm_config, dict):
+ llm_config = CONFIG_MAPPING[llm_config["model_type"]](**llm_config)
+
+ self.llm_config = llm_config
+
+ super().__init__(**kwargs)
+ self.audio_vocab_size = audio_vocab_size
+ self.audio_mask_id = audio_mask_id
+ self.num_audio_codebook = num_audio_codebook
+ if audio_codebook_weights is None:
+ audio_codebook_weights = [8, 8, 6, 6, 4, 4, 2, 2]
+ self.audio_codebook_weights = audio_codebook_weights
+
+
+class OmniVoice(PreTrainedModel):
+ _supports_flex_attn = True
+ _supports_flash_attn_2 = True
+ config_class = OmniVoiceConfig
+
+ def __init__(self, config: OmniVoiceConfig, llm: Optional[PreTrainedModel] = None):
+ super().__init__(config)
+
+ if llm is not None:
+ # If an LLM instance is provided, use it directly
+ # (skipping config-based init).
+ self.llm = llm
+ else:
+ # Otherwise, initialize the LLM from the config.
+ self.llm = AutoModel.from_config(self.config.llm_config)
+
+ self.audio_embeddings = nn.Embedding(
+ config.num_audio_codebook * config.audio_vocab_size,
+ self.config.llm_config.hidden_size,
+ )
+ self.register_buffer(
+ "codebook_layer_offsets",
+ torch.arange(config.num_audio_codebook) * config.audio_vocab_size,
+ )
+
+ self.audio_heads = nn.Linear(
+ self.config.llm_config.hidden_size,
+ config.num_audio_codebook * config.audio_vocab_size,
+ bias=False,
+ )
+
+ self.normalized_audio_codebook_weights = [
+ w / sum(config.audio_codebook_weights)
+ for w in config.audio_codebook_weights
+ ]
+
+ self.post_init()
+
+ # Inference-only attributes (set by from_pretrained when not in train mode)
+ self.text_tokenizer = None
+ self.audio_tokenizer = None
+ self.duration_estimator = None
+ self.sampling_rate = None
+ self._asr_pipe = None
+
+ @classmethod
+ def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
+ train_mode = kwargs.pop("train", False)
+ load_asr = kwargs.pop("load_asr", False)
+ asr_model_name = kwargs.pop("asr_model_name", "openai/whisper-large-v3-turbo")
+
+ # Suppress noisy INFO logs from transformers/huggingface_hub during loading
+ _prev_disable = logging.root.manager.disable
+ logging.disable(logging.INFO)
+
+ try:
+ model = super().from_pretrained(
+ pretrained_model_name_or_path, *args, **kwargs
+ )
+
+ if not train_mode:
+ # Resolve local path for audio tokenizer subdirectory
+ if os.path.isdir(pretrained_model_name_or_path):
+ resolved_path = pretrained_model_name_or_path
+ else:
+ from huggingface_hub import snapshot_download
+
+ resolved_path = snapshot_download(pretrained_model_name_or_path)
+
+ model.text_tokenizer = AutoTokenizer.from_pretrained(
+ pretrained_model_name_or_path
+ )
+
+ audio_tokenizer_path = os.path.join(resolved_path, "audio_tokenizer")
+
+ if not os.path.isdir(audio_tokenizer_path):
+ # Fallback to the HuggingFace Hub path of transformers'
+ # HiggsAudioV2Tokenizer if the local subdirectory doesn't exist.
+ audio_tokenizer_path = "eustlb/higgs-audio-v2-tokenizer"
+
+ # higgs-audio-v2-tokenizer does not support MPS (output channels > 65536)
+ tokenizer_device = (
+ "cpu" if str(model.device).startswith("mps") else model.device
+ )
+ model.audio_tokenizer = HiggsAudioV2TokenizerModel.from_pretrained(
+ audio_tokenizer_path, device_map=tokenizer_device
+ )
+ model.feature_extractor = AutoFeatureExtractor.from_pretrained(
+ audio_tokenizer_path
+ )
+
+ model.sampling_rate = model.feature_extractor.sampling_rate
+
+ model.duration_estimator = RuleDurationEstimator()
+
+ if load_asr:
+ model.load_asr_model(model_name=asr_model_name)
+ finally:
+ logging.disable(_prev_disable)
+
+ return model
+
+ # -------------------------------------------------------------------
+ # ASR support (optional, for auto-transcription)
+ # -------------------------------------------------------------------
+
+ def load_asr_model(self, model_name: str = "openai/whisper-large-v3-turbo"):
+ """Load a Whisper ASR model for reference audio transcription.
+
+ Args:
+ model_name: HuggingFace model name for the Whisper model.
+ """
+ from transformers import pipeline as hf_pipeline
+
+ logger.info("Loading ASR model %s ...", model_name)
+ asr_dtype = (
+ torch.float16 if str(self.device).startswith("cuda") else torch.float32
+ )
+ self._asr_pipe = hf_pipeline(
+ "automatic-speech-recognition",
+ model=model_name,
+ dtype=asr_dtype,
+ device_map=self.device,
+ )
+ logger.info("ASR model loaded on %s.", self.device)
+
+ @torch.inference_mode()
+ def transcribe(
+ self,
+ audio: Union[str, tuple[torch.Tensor, int]],
+ ) -> str:
+ """Transcribe audio using the loaded Whisper ASR model.
+
+ Args:
+ audio: File path or (waveform, sample_rate) tuple.
+
+ Returns:
+ Transcribed text.
+ """
+ if self._asr_pipe is None:
+ raise RuntimeError(
+ "ASR model is not loaded. Call model.load_asr_model() first."
+ )
+
+ if isinstance(audio, str):
+ return self._asr_pipe(audio)["text"].strip()
+ else:
+ waveform, sr = audio
+ if waveform.dim() == 1:
+ waveform = waveform.unsqueeze(0)
+ if waveform.size(0) > 1:
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
+ audio_input = {
+ "array": waveform.squeeze(0).cpu().numpy(),
+ "sampling_rate": sr,
+ }
+ return self._asr_pipe(audio_input)["text"].strip()
+
+ def get_input_embeddings(self):
+ return self.llm.get_input_embeddings()
+
+ def set_input_embeddings(self, value):
+ self.llm.set_input_embeddings(value)
+
+ def _prepare_embed_inputs(
+ self, input_ids: torch.Tensor, audio_mask: torch.Tensor
+ ) -> torch.Tensor:
+ """
+ Prepares embeddings from input_ids of shape (batch_size, layers, seq_length).
+ Embedding shape is (batch_size, seq_length, hidden_size).
+ """
+ text_embeds = self.get_input_embeddings()(input_ids[:, 0, :])
+
+ # Apply shift to audio IDs based on codebook layer
+ # audio_ids: [Batch, 8, Seq]
+ # codebook_layer_offsets: [1, 8, 1]
+ # Result: Layer 0 ID Layer 1 ID + Layer 2 ID + 2050...
+ shifted_ids = (
+ input_ids * audio_mask.unsqueeze(1)
+ ) + self.codebook_layer_offsets.view(1, -1, 1)
+
+ # input: [Batch, 8, Seq] -> output: [Batch, Seq, Hidden]
+ audio_embeds = self.audio_embeddings(shifted_ids).sum(dim=1)
+
+ return torch.where(audio_mask.unsqueeze(-1), audio_embeds, text_embeds)
+
+ def forward(
+ self,
+ input_ids: torch.LongTensor,
+ audio_mask: torch.Tensor,
+ labels: Optional[torch.LongTensor] = None,
+ attention_mask: Optional[torch.Tensor] = None,
+ document_ids: Optional[torch.Tensor] = None,
+ position_ids: Optional[torch.LongTensor] = None,
+ ):
+
+ inputs_embeds = self._prepare_embed_inputs(input_ids, audio_mask)
+
+ if attention_mask is None and document_ids is not None:
+ attention_mask = create_block_mask(
+ _get_packed_mask(
+ document_ids[0].to(inputs_embeds.device),
+ ),
+ B=None,
+ H=None,
+ Q_LEN=input_ids.size(-1),
+ KV_LEN=input_ids.size(-1),
+ _compile=True,
+ device=inputs_embeds.device,
+ )
+
+ llm_outputs = self.llm(
+ inputs_embeds=inputs_embeds,
+ attention_mask=attention_mask,
+ return_dict=True,
+ position_ids=position_ids,
+ )
+ hidden_states = llm_outputs[0]
+
+ loss = None
+
+ # Shape: [B, S, C * Vocab]
+ batch_size, seq_len, _ = hidden_states.shape
+ logits_flat = self.audio_heads(hidden_states)
+ # Shape: [B, S, C, Vocab] -> [B, C, S, Vocab]
+ audio_logits = logits_flat.view(
+ batch_size,
+ seq_len,
+ self.config.num_audio_codebook,
+ self.config.audio_vocab_size,
+ ).permute(0, 2, 1, 3)
+
+ if labels is not None:
+
+ # audio_logits.permute(0, 3, 1, 2):
+ # [Batch, Layer, Seq, Vocab] -> [Batch, Vocab, Layer, Seq]
+ # per_token_loss shape: [Batch, Layer, Seq],ignore -100
+ per_token_loss = torch.nn.functional.cross_entropy(
+ audio_logits.permute(0, 3, 1, 2),
+ labels,
+ reduction="none",
+ ignore_index=-100,
+ )
+ # valid_mask shape: [Batch, Layer, Seq]
+ valid_mask = (labels != -100).float()
+
+ # layer_means shape: [num_layers]
+ layer_means = (per_token_loss * valid_mask).sum(
+ dim=(0, 2)
+ ) / valid_mask.sum(dim=(0, 2)).clamp(min=1.0)
+
+ weights = torch.tensor(
+ self.normalized_audio_codebook_weights, device=audio_logits.device
+ )
+ loss = (layer_means * weights).sum()
+
+ return OmniVoiceModelOutput(
+ loss=loss,
+ logits=audio_logits,
+ )
+
+ def supported_language_ids(self) -> set[str]:
+ """Return a list of supported language IDs."""
+ return LANG_IDS
+
+ def supported_language_names(self) -> set[str]:
+ """Return a list of supported language names."""
+ return LANG_NAMES
+
+ # -------------------------------------------------------------------
+ # Inference API
+ # -------------------------------------------------------------------
+
+ @torch.inference_mode()
+ def generate(
+ self,
+ text: Union[str, list[str]],
+ language: Union[str, list[str], None] = None,
+ ref_text: Union[str, list[str], None] = None,
+ ref_audio: Union[
+ str,
+ list[str],
+ tuple[torch.Tensor, int],
+ list[tuple[torch.Tensor, int]],
+ None,
+ ] = None,
+ voice_clone_prompt: Union[
+ VoiceClonePrompt, list[VoiceClonePrompt], None
+ ] = None,
+ instruct: Union[str, list[str], None] = None,
+ duration: Union[float, list[Optional[float]], None] = None,
+ speed: Union[float, list[Optional[float]], None] = None,
+ generation_config: Optional[OmniVoiceGenerationConfig] = None,
+ **kwargs,
+ ) -> list[torch.Tensor]:
+ """Generate speech audio given text in various modes.
+
+ Supports three modes:
+
+ 1. **Voice clone** — clone the voice style from the reference audio.
+ Should provide ``voice_clone_prompt`` (from
+ :meth:`create_voice_clone_prompt`) or ``ref_text`` + ``ref_audio``.
+ 2. **Voice design** — provide ``instruct`` text describing
+ the desired voice style; no reference audio needed.
+ 3. **Auto** — provide neither; the model picks a voice itself.
+
+ Args:
+ text: Target text (single string or list for batch).
+ language: Language name (e.g. ``"English"``) or code
+ (e.g. ``"en"``). ``None`` for language-agnostic mode.
+ Performance is slightly better if you specify the language.
+ ref_text: Optional reference text for voice cloning mode.
+ ref_audio: Optional reference audio for voice cloning mode.
+ Can be a file path or a (waveform, sample_rate) tuple.
+ voice_clone_prompt: Reusable prompt from :meth:`create_voice_clone_prompt`.
+ If provided, it overrides ``ref_text`` and ``ref_audio``.
+ instruct: Style instruction for voice design mode.
+ duration: Fixed output duration in seconds. If a single float,
+ applies to all items; if a list, one value per item.
+ ``None`` (default) lets the model estimate duration from text.
+ Overrides ``speed`` when both are provided.
+ speed: Speaking speed factor. ``> 1.0`` for faster, ``< 1.0`` for
+ slower. If a list, one value per item. ``None`` (default) uses
+ the model's default estimation.
+ generation_config: Explicit config object. If provided, takes
+ precedence over ``**kwargs``.
+ **kwargs: Generation config or its fields:
+ denoise: Whether to prepend the ``<|denoise|>`` token.
+ num_step: Number of iterative decoding steps.
+ guidance_scale: Classifier-free guidance scale.
+ t_shift: Time-step shift (smaller → emphasise low-SNR).
+ postprocess_output: Post-process output (remove silence, fade-in/out, pad edges).
+ layer_penalty_factor: Penalty encouraging earlier codebook
+ layers to unmask first.
+ position_temperature: Temperature for position selection.
+ class_temperature: Temperature for token sampling (0 = greedy).
+ audio_chunk_duration: If > 0, split long text into chunks of
+ this duration (seconds) and generate chunk by chunk.
+ audio_chunk_threshold: Only apply chunking if estimated audio
+ duration exceeds this threshold (seconds).
+ Returns:
+ ``audios`` a list of 2-D ``torch.Tensor``, with the shape (1, T) and sampling rate
+ consistent with the model's audio tokenizer (usually 24000 Hz).
+ """
+
+ if self.audio_tokenizer is None or self.text_tokenizer is None:
+ raise RuntimeError(
+ "Model is not loaded with audio/text tokenizers. Make sure you "
+ "loaded the model with OmniVoice.from_pretrained()."
+ )
+ gen_config = (
+ generation_config
+ if generation_config is not None
+ else OmniVoiceGenerationConfig.from_dict(kwargs)
+ )
+
+ self.eval()
+
+ full_task = self._preprocess_all(
+ text=text,
+ language=language,
+ ref_text=ref_text,
+ ref_audio=ref_audio,
+ voice_clone_prompt=voice_clone_prompt,
+ instruct=instruct,
+ preprocess_prompt=gen_config.preprocess_prompt,
+ speed=speed,
+ duration=duration,
+ )
+
+ short_idx, long_idx = full_task.get_indices(
+ gen_config, self.audio_tokenizer.config.frame_rate
+ )
+
+ results = [None] * full_task.batch_size
+
+ if short_idx:
+ short_task = full_task.slice_task(short_idx)
+ short_results = self._generate_iterative(short_task, gen_config)
+ for idx, res in zip(short_idx, short_results):
+ results[idx] = res
+
+ if long_idx:
+ long_task = full_task.slice_task(long_idx)
+ long_results = self._generate_chunked(long_task, gen_config)
+ for idx, res in zip(long_idx, long_results):
+ results[idx] = res
+
+ generated_audios = []
+ for i in range(full_task.batch_size):
+ assert results[i] is not None, f"Result {i} was not generated"
+ generated_audios.append(
+ self._decode_and_post_process(
+ results[i], full_task.ref_rms[i], gen_config # type: ignore[arg-type]
+ )
+ )
+
+ return generated_audios
+
+ def create_voice_clone_prompt(
+ self,
+ ref_audio: Union[str, tuple[torch.Tensor, int]],
+ ref_text: Optional[str] = None,
+ preprocess_prompt: bool = True,
+ ) -> VoiceClonePrompt:
+ """Create a reusable voice clone prompt from reference audio.
+
+ Args:
+ ref_audio: File path (str) or ``(waveform, sample_rate)`` tuple.
+ waveform should be a 1-D or 2-D torch.Tensor (channels x samples).
+ ref_text: Transcript of the reference audio. If ``None``, the
+ ASR model will be used to auto-transcribe (must call
+ :meth:`load_asr_model` first).
+ preprocess_prompt: If ``True`` (default), apply silence removal and
+ trimming to the reference audio, add punctuation in the end
+ of reference text (if not already)
+
+ Returns:
+ A :class:`VoiceClonePrompt` that can be passed to :meth:`generate`.
+ """
+ if self.audio_tokenizer is None:
+ raise RuntimeError(
+ "Audio tokenizer is not loaded. Make sure you loaded the model "
+ "with OmniVoice.from_pretrained()."
+ )
+
+ if isinstance(ref_audio, str):
+ ref_wav = load_audio(ref_audio, self.sampling_rate)
+ else:
+ waveform, sr = ref_audio
+ if waveform.dim() == 1:
+ waveform = waveform.unsqueeze(0)
+ if waveform.size(0) > 1:
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
+ if sr != self.sampling_rate:
+ waveform = torchaudio.functional.resample(
+ waveform, sr, self.sampling_rate
+ )
+ ref_wav = waveform
+
+ ref_rms = torch.sqrt(torch.mean(torch.square(ref_wav))).item()
+ if 0 < ref_rms < 0.1:
+ ref_wav = ref_wav * 0.1 / ref_rms
+
+ if preprocess_prompt:
+ # Trim long reference audio (>20s) by splitting at the largest silence gap.
+ # Skip trimming when ref_text is user-provided, otherwise the
+ # trimmed audio will no longer match the full transcript.
+ if ref_text is None:
+ ref_wav = trim_long_audio(
+ ref_wav, self.sampling_rate, trim_threshold=20.0
+ )
+ ref_wav = remove_silence(
+ ref_wav,
+ self.sampling_rate,
+ mid_sil=200,
+ lead_sil=100,
+ trail_sil=200,
+ )
+ if ref_wav.size(-1) == 0:
+ raise ValueError(
+ "Reference audio is empty after silence removal. "
+ "Try setting preprocess_prompt=False."
+ )
+
+ ref_duration = ref_wav.size(-1) / self.sampling_rate
+ if ref_duration > 20.0:
+ logger.warning(
+ "Reference audio is %.1fs long (>20s). This may cause slower "
+ "generation, higher memory usage, and degraded voice cloning "
+ "quality. We recommend trimming it to 3-10s.",
+ ref_duration,
+ )
+
+ # Auto-transcribe if ref_text not provided
+ if ref_text is None:
+ if self._asr_pipe is None:
+ logger.info("ASR model not loaded yet, loading on-the-fly ...")
+ self.load_asr_model()
+ ref_text = self.transcribe((ref_wav, self.sampling_rate))
+ logger.debug("Auto-transcribed ref_text: %s", ref_text)
+
+ chunk_size = self.audio_tokenizer.config.hop_length
+ clip_size = int(ref_wav.size(-1) % chunk_size)
+ ref_wav = ref_wav[:, :-clip_size] if clip_size > 0 else ref_wav
+ ref_audio_tokens = self.audio_tokenizer.encode(
+ ref_wav.unsqueeze(0).to(self.audio_tokenizer.device),
+ ).audio_codes.squeeze(
+ 0
+ ) # (C, T)
+
+ if preprocess_prompt:
+ ref_text = add_punctuation(ref_text)
+
+ return VoiceClonePrompt(
+ ref_audio_tokens=ref_audio_tokens,
+ ref_text=ref_text,
+ ref_rms=ref_rms,
+ )
+
+ def _decode_and_post_process(
+ self,
+ tokens: Union[torch.Tensor, List[torch.Tensor]],
+ rms: Union[float, None],
+ gen_config: OmniVoiceGenerationConfig,
+ ) -> torch.Tensor:
+ """
+ Args:
+ tokens: Audio tokens — either a single tensor of shape
+ (num_codebooks, seq_len) or a list of chunk tensors.
+ rms: RMS of the reference audio for volume adjustment.
+ gen_config: Generation config for post-processing options.
+ Returns:
+ Decoded and post-processed audio tensor of shape (1, T).
+ """
+ tokenizer_device = self.audio_tokenizer.device
+ if isinstance(tokens, list):
+ chunk_audios = [
+ self.audio_tokenizer.decode(t.to(tokenizer_device).unsqueeze(0))
+ .audio_values[0]
+ .cpu()
+ for t in tokens
+ ]
+ audio_waveform = cross_fade_chunks(chunk_audios, self.sampling_rate)
+ else:
+ audio_waveform = (
+ self.audio_tokenizer.decode(tokens.to(tokenizer_device).unsqueeze(0))
+ .audio_values[0]
+ .cpu()
+ )
+
+ return self._post_process_audio(
+ audio_waveform,
+ postprocess_output=gen_config.postprocess_output,
+ ref_rms=rms,
+ )
+
+ def _post_process_audio(
+ self,
+ generated_audio: torch.Tensor,
+ postprocess_output: bool,
+ ref_rms: Union[float, None],
+ ) -> torch.Tensor:
+ """Optionally remove long silences, adjust volume, and add edge padding.
+
+ Args:
+ generated_audio: Audio tensor of shape (1, T).
+ postprocess_output: If True, remove long silences and apply fade/pad.
+ ref_rms: RMS of the reference audio for volume normalisation.
+ Returns:
+ Processed audio tensor of shape (1, T).
+ """
+ if postprocess_output:
+ generated_audio = remove_silence(
+ generated_audio,
+ self.sampling_rate,
+ mid_sil=500,
+ lead_sil=100,
+ trail_sil=100,
+ )
+
+ if ref_rms is not None and ref_rms < 0.1:
+ generated_audio = generated_audio * ref_rms / 0.1
+ elif ref_rms is None:
+ # No reference audio (voice design): peak-normalize to 0.5
+ # to avoid clipping while keeping a comfortable volume level.
+ peak = generated_audio.abs().max()
+ if peak > 1e-6:
+ generated_audio = generated_audio / peak * 0.5
+
+ generated_audio = fade_and_pad_audio(
+ generated_audio,
+ sample_rate=self.sampling_rate,
+ )
+ return generated_audio
+
+ def _generate_chunked(
+ self, task: GenerationTask, gen_config: OmniVoiceGenerationConfig
+ ) -> List[List[torch.Tensor]]:
+ """Generate long audio by splitting text into chunks and batching.
+
+ Each item in the returned list corresponds to one input and contains
+ a list of audio token tensors — one per text chunk.
+
+ Args:
+ task: A :class:`GenerationTask` with one or more items whose
+ estimated audio exceeds ``audio_chunk_threshold``.
+ gen_config: Generation config (``audio_chunk_duration`` controls
+ chunk size).
+ Returns:
+ Per-item list of chunk token-tensor lists.
+ """
+ # Chunk each item's text
+ all_chunks = []
+ for i in range(task.batch_size):
+ avg_tokens_per_char = task.target_lens[i] / len(task.texts[i])
+ text_chunk_len = int(
+ gen_config.audio_chunk_duration
+ * self.audio_tokenizer.config.frame_rate
+ / avg_tokens_per_char
+ )
+ chunks = chunk_text_punctuation(
+ text=task.texts[i],
+ chunk_len=text_chunk_len,
+ min_chunk_len=3,
+ )
+ logger.debug(f"Item {i} chunked into {len(chunks)} pieces: {chunks}")
+ all_chunks.append(chunks)
+
+ has_ref = [t is not None for t in task.ref_audio_tokens]
+ assert all(has_ref) or not any(has_ref), (
+ "Chunked inference requires all items to either have or not have "
+ "ref_audio. Mixed ref/non-ref is not supported."
+ )
+
+ max_num_chunks = max(len(c) for c in all_chunks)
+
+ # chunk_results[item_idx] = list of generated token tensors per chunk
+ chunk_results = [[] for _ in range(task.batch_size)]
+
+ def _run_batch(indices, texts, ref_audios, ref_texts):
+ speed_list = task.speed
+ target_lens = [
+ self._estimate_target_tokens(
+ texts[j],
+ ref_texts[j],
+ ref_audios[j].size(-1) if ref_audios[j] is not None else None,
+ speed=speed_list[i] if speed_list else 1.0,
+ )
+ for j, i in enumerate(indices)
+ ]
+ sub_task = GenerationTask(
+ batch_size=len(indices),
+ texts=texts,
+ target_lens=target_lens,
+ langs=[task.langs[i] for i in indices],
+ instructs=[task.instructs[i] for i in indices],
+ ref_texts=ref_texts,
+ ref_audio_tokens=ref_audios,
+ ref_rms=[task.ref_rms[i] for i in indices],
+ speed=[task.speed[i] for i in indices] if task.speed else None,
+ )
+ gen_tokens = self._generate_iterative(sub_task, gen_config)
+ for j, idx in enumerate(indices):
+ chunk_results[idx].append(gen_tokens[j])
+
+ if all(has_ref):
+ # All items have reference audio.
+ # We still sequentially generate chunks within each item, but we
+ # batch across items for the same chunk index. This allows to keep
+ # the VRAM usage manageable while still benefiting from batching.
+ for ci in range(max_num_chunks):
+ indices = [i for i in range(task.batch_size) if ci < len(all_chunks[i])]
+ if not indices:
+ continue
+ _run_batch(
+ indices,
+ texts=[all_chunks[i][ci] for i in indices],
+ ref_audios=[task.ref_audio_tokens[i] for i in indices],
+ ref_texts=[task.ref_texts[i] for i in indices],
+ )
+ else:
+ # No reference audio — generate chunk 0 for all items first,
+ # then use chunk 0 output as reference for all subsequent chunks.
+ indices_0 = [i for i in range(task.batch_size) if len(all_chunks[i]) > 0]
+ _run_batch(
+ indices_0,
+ texts=[all_chunks[i][0] for i in indices_0],
+ ref_audios=[None] * len(indices_0),
+ ref_texts=[None] * len(indices_0),
+ )
+ first_chunk_map = {idx: chunk_results[idx][0] for idx in indices_0}
+
+ # Batch all remaining chunks, using chunk 0 as fixed reference
+ for ci in range(1, max_num_chunks):
+ indices = [i for i in range(task.batch_size) if ci < len(all_chunks[i])]
+ if not indices:
+ continue
+ _run_batch(
+ indices,
+ texts=[all_chunks[i][ci] for i in indices],
+ ref_audios=[first_chunk_map[i] for i in indices],
+ ref_texts=[all_chunks[i][0] for i in indices],
+ )
+
+ return chunk_results
+
+ def _preprocess_all(
+ self,
+ text: Union[str, list[str]],
+ language: Union[str, list[str], None] = None,
+ ref_text: Union[str, list[str], None] = None,
+ ref_audio: Union[
+ str,
+ list[str],
+ tuple[torch.Tensor, int],
+ list[tuple[torch.Tensor, int]],
+ None,
+ ] = None,
+ voice_clone_prompt: Union[
+ VoiceClonePrompt, list[VoiceClonePrompt], None
+ ] = None,
+ instruct: Union[str, list[str], None] = None,
+ preprocess_prompt: bool = True,
+ speed: Union[float, list[Optional[float]], None] = None,
+ duration: Union[float, list[Optional[float]], None] = None,
+ ) -> GenerationTask:
+
+ if isinstance(text, str):
+ text_list = [text]
+ else:
+ assert isinstance(
+ text, list
+ ), "text should be a string or a list of strings"
+ text_list = text
+ batch_size = len(text_list)
+
+ language_list = self._ensure_list(language, batch_size)
+ language_list = [_resolve_language(lang) for lang in language_list]
+ instruct_list = self._ensure_list(instruct, batch_size)
+ for i, s in enumerate(instruct_list):
+ if s is None:
+ continue
+ use_zh = bool(text_list[i] and _ZH_RE.search(text_list[i]))
+ instruct_list[i] = _resolve_instruct(s, use_zh=use_zh)
+
+ if voice_clone_prompt is not None and (
+ ref_text is not None or ref_audio is not None
+ ):
+ logger.warning(
+ "Both voice_clone_prompt and ref_text/ref_audio are provided. "
+ "ref_text/ref_audio will be ignored."
+ )
+ if voice_clone_prompt is None and ref_audio is not None:
+ # If voice_clone_prompt is not provided, create it from
+ # ref_audio (ref_text will be auto-transcribed if not given).
+ ref_text_list = self._ensure_list(ref_text, batch_size, auto_repeat=False)
+ ref_audio_list = self._ensure_list(ref_audio, batch_size, auto_repeat=False)
+
+ voice_clone_prompt = []
+ for i in range(len(ref_text_list)):
+ voice_clone_prompt.append(
+ self.create_voice_clone_prompt(
+ ref_audio=ref_audio_list[i],
+ ref_text=ref_text_list[i],
+ preprocess_prompt=preprocess_prompt,
+ )
+ )
+
+ voice_clone_prompt_list = self._ensure_list(voice_clone_prompt, batch_size)
+ if voice_clone_prompt_list[0] is not None:
+ ref_text_list = [vc.ref_text for vc in voice_clone_prompt_list]
+ ref_audio_tokens_list = [
+ vc.ref_audio_tokens for vc in voice_clone_prompt_list
+ ]
+ ref_rms_list = [vc.ref_rms for vc in voice_clone_prompt_list]
+ else:
+ ref_text_list = [None] * batch_size
+ ref_audio_tokens_list = [None] * batch_size
+ ref_rms_list = [None] * batch_size
+
+ # Normalize speed/duration to per-item lists (may contain None).
+ if speed is not None:
+ if isinstance(speed, (int, float)):
+ user_speed = [float(speed)] * batch_size
+ else:
+ user_speed = list(speed)
+ else:
+ user_speed = None
+
+ if duration is not None:
+ if isinstance(duration, (int, float)):
+ durations = [float(duration)] * batch_size
+ else:
+ durations = list(duration)
+ else:
+ durations = None
+
+ num_target_tokens_list = []
+ for i in range(batch_size):
+ # duration[i] overrides speed for estimation: use speed=1.0
+ # to get the raw estimate, then override target_lens below.
+ has_dur = durations is not None and durations[i] is not None
+ item_speed = 1.0 if has_dur else (user_speed[i] if user_speed else 1.0)
+ est = self._estimate_target_tokens(
+ text_list[i],
+ ref_text_list[i],
+ ref_audio_tokens_list[i].size(-1)
+ if ref_audio_tokens_list[i] is not None
+ else None,
+ speed=item_speed,
+ )
+ num_target_tokens_list.append(est)
+
+ # Per-item duration overrides: set target_lens to exact frame count
+ # and compute speed ratio so chunked generation scales proportionally.
+ speed_list: Optional[List[float]] = None
+ if durations is not None:
+ frame_rate = self.audio_tokenizer.config.frame_rate
+ speed_list = []
+ for i in range(batch_size):
+ if durations[i] is not None:
+ target_tokens = max(1, int(durations[i] * frame_rate))
+ est = num_target_tokens_list[i]
+ speed_list.append(est / target_tokens if target_tokens > 0 else 1.0)
+ num_target_tokens_list[i] = target_tokens
+ else:
+ s = user_speed[i] if user_speed else None
+ speed_list.append(s if s is not None else 1.0)
+ elif user_speed is not None:
+ speed_list = [s if s is not None else 1.0 for s in user_speed]
+
+ return GenerationTask(
+ batch_size=batch_size,
+ texts=text_list,
+ target_lens=num_target_tokens_list,
+ langs=language_list,
+ instructs=instruct_list,
+ ref_texts=ref_text_list,
+ ref_audio_tokens=ref_audio_tokens_list,
+ ref_rms=ref_rms_list,
+ speed=speed_list,
+ )
+
+ def _estimate_target_tokens(self, text, ref_text, num_ref_audio_tokens, speed=1.0):
+ """Estimate number of target audio tokens."""
+ if num_ref_audio_tokens is None or ref_text is None or len(ref_text) == 0:
+ # Fall back to a simple heuristic
+ ref_text = "Nice to meet you."
+ num_ref_audio_tokens = 25
+
+ est = self.duration_estimator.estimate_duration(
+ text, ref_text, num_ref_audio_tokens
+ )
+ if speed > 0 and speed != 1.0:
+ est = est / speed
+ return max(1, int(est))
+
+ def _ensure_list(
+ self, x: Union[Any, List[Any]], batch_size: int, auto_repeat: bool = True
+ ) -> List[Any]:
+ x_list = x if isinstance(x, list) else [x]
+ if len(x_list) not in (
+ 1,
+ batch_size,
+ ):
+ raise ValueError(
+ f"should be either the number of the text or 1, but got {len(x_list)}"
+ )
+ if auto_repeat and len(x_list) == 1 and batch_size is not None:
+ x_list = x_list * batch_size
+ return x_list
+
+ def _prepare_inference_inputs(
+ self,
+ text: str,
+ num_target_tokens: int,
+ ref_text: Optional[str] = None,
+ ref_audio_tokens: Optional[torch.Tensor] = None,
+ lang: Optional[str] = None,
+ instruct: Optional[str] = None,
+ denoise: bool = True,
+ ):
+ """Prepare input_ids and audio masks for inference.
+ Args:
+ text: Target text to generate.
+ num_target_tokens: Number of audio tokens to generate.
+ ref_text: Optional reference text for voice cloning.
+ ref_audio_tokens: Optional reference audio tokens for voice cloning.
+ with shape (C, T).
+ lang: Optional language ID.
+ instruct: Optional style instruction for voice design.
+ denoise: Whether to include the <|denoise|> token.
+ """
+
+ # Build style tokens: <|denoise|> + <|lang_start|>...<|lang_end|>
+ # + <|instruct_start|>...<|instruct_end|>
+ style_text = ""
+ if denoise and ref_audio_tokens is not None:
+ style_text += "<|denoise|>"
+ lang_str = lang if lang else "None"
+ instruct_str = instruct if instruct else "None"
+ style_text += f"<|lang_start|>{lang_str}<|lang_end|>"
+ style_text += f"<|instruct_start|>{instruct_str}<|instruct_end|>"
+
+ style_tokens = (
+ self.text_tokenizer(style_text, return_tensors="pt")
+ .input_ids.repeat(self.config.num_audio_codebook, 1)
+ .unsqueeze(0)
+ ).to(
+ self.device
+ ) # [1, C, N1]
+
+ # Build text tokens
+ full_text = _combine_text(ref_text=ref_text, text=text)
+ wrapped_text = f"<|text_start|>{full_text}<|text_end|>"
+ text_tokens = (
+ _tokenize_with_nonverbal_tags(wrapped_text, self.text_tokenizer)
+ .repeat(self.config.num_audio_codebook, 1)
+ .unsqueeze(0)
+ ).to(
+ self.device
+ ) # [1, C, N2]
+
+ # Target: all MASK
+ target_audio_tokens = torch.full(
+ (1, self.config.num_audio_codebook, num_target_tokens),
+ self.config.audio_mask_id,
+ dtype=torch.long,
+ device=self.device,
+ )
+
+ # Conditional input
+ parts = [style_tokens, text_tokens]
+ if ref_audio_tokens is not None:
+ parts.append(ref_audio_tokens.unsqueeze(0).to(self.device))
+ parts.append(target_audio_tokens)
+ cond_input_ids = torch.cat(parts, dim=2)
+
+ cond_total_length = cond_input_ids.shape[2]
+ cond_audio_start_idx = cond_total_length - num_target_tokens
+ if ref_audio_tokens is not None:
+ cond_audio_start_idx -= ref_audio_tokens.size(-1)
+
+ cond_audio_mask = torch.zeros(
+ 1, cond_total_length, dtype=torch.bool, device=self.device
+ )
+ cond_audio_mask[0, cond_audio_start_idx:] = True
+
+ return {
+ "input_ids": cond_input_ids,
+ "audio_mask": cond_audio_mask,
+ }
+
+ def _generate_iterative(
+ self, task: GenerationTask, gen_config: OmniVoiceGenerationConfig
+ ) -> List[torch.Tensor]:
+ """N-step iterative unmasked decoding.
+
+ Args:
+ task: A :class:`GenerationTask` containing batch texts, target
+ lengths, languages, instructions, and optional reference data.
+ gen_config: A :class:`OmniVoiceGenerationConfig` controlling
+ decoding steps, guidance, temperatures, etc.
+ Returns:
+ List of generated audio token tensors of shape (C, T) (one per
+ input text).
+ """
+
+ B = task.batch_size
+
+ for i in range(B):
+ logger.debug(
+ "Item %d — text: %s | ref_text: %s | instruct: %s | lang: %s | target_tokens: %d",
+ i,
+ task.texts[i],
+ task.ref_texts[i],
+ task.instructs[i],
+ task.langs[i],
+ task.target_lens[i],
+ )
+
+ inputs_list = [
+ self._prepare_inference_inputs(
+ task.texts[i],
+ task.target_lens[i],
+ task.ref_texts[i],
+ task.ref_audio_tokens[i],
+ task.langs[i],
+ task.instructs[i],
+ gen_config.denoise,
+ )
+ for i in range(B)
+ ]
+
+ c_lens = [inp["input_ids"].size(2) for inp in inputs_list]
+ max_c_len = max(c_lens)
+ pad_id = self.config.audio_mask_id # Or any other tokens
+
+ batch_input_ids = torch.full(
+ (2 * B, self.config.num_audio_codebook, max_c_len),
+ pad_id,
+ dtype=torch.long,
+ device=self.device,
+ )
+ batch_audio_mask = torch.zeros(
+ (2 * B, max_c_len), dtype=torch.bool, device=self.device
+ )
+ batch_attention_mask = torch.zeros(
+ (2 * B, 1, max_c_len, max_c_len), dtype=torch.bool, device=self.device
+ )
+
+ for i, inp in enumerate(inputs_list):
+ c_len, u_len = c_lens[i], task.target_lens[i]
+
+ # Cond (0 ~ B-1)
+ batch_input_ids[i, :, :c_len] = inp["input_ids"]
+ batch_audio_mask[i, :c_len] = inp["audio_mask"]
+ batch_attention_mask[i, :, :c_len, :c_len] = True
+
+ # Uncond (B ~ 2B-1)
+ batch_input_ids[B + i, :, :u_len] = inp["input_ids"][..., -u_len:]
+ batch_audio_mask[B + i, :u_len] = inp["audio_mask"][..., -u_len:]
+ batch_attention_mask[B + i, :, :u_len, :u_len] = True
+ if max_c_len > u_len:
+ pad_diag = torch.arange(u_len, max_c_len, device=self.device)
+ batch_attention_mask[B + i, :, pad_diag, pad_diag] = True
+
+ tokens = torch.full(
+ (B, self.config.num_audio_codebook, max(task.target_lens)),
+ self.config.audio_mask_id,
+ dtype=torch.long,
+ device=self.device,
+ )
+
+ timesteps = _get_time_steps(
+ t_start=0.0,
+ t_end=1.0,
+ num_step=gen_config.num_step + 1,
+ t_shift=gen_config.t_shift,
+ ).tolist()
+ schedules = []
+ for t_len in task.target_lens:
+ total_mask = t_len * self.config.num_audio_codebook
+ rem = total_mask
+ sched = []
+ for step in range(gen_config.num_step):
+ num = (
+ rem
+ if step == gen_config.num_step - 1
+ else min(
+ math.ceil(total_mask * (timesteps[step + 1] - timesteps[step])),
+ rem,
+ )
+ )
+ sched.append(int(num))
+ rem -= int(num)
+ schedules.append(sched)
+
+ layer_ids = torch.arange(
+ self.config.num_audio_codebook, device=self.device
+ ).view(1, -1, 1)
+
+ for step in range(gen_config.num_step):
+ batch_logits = self(
+ input_ids=batch_input_ids,
+ audio_mask=batch_audio_mask,
+ attention_mask=batch_attention_mask,
+ ).logits.to(torch.float32)
+
+ for i in range(B):
+ k = schedules[i][step]
+ if k <= 0:
+ continue
+
+ c_len, t_len = c_lens[i], task.target_lens[i]
+
+ # Extract real target Logits
+ # [1, C, T, V]
+ c_logits = batch_logits[i : i + 1, :, c_len - t_len : c_len, :]
+ u_logits = batch_logits[B + i : B + i + 1, :, :t_len, :]
+
+ pred_tokens, scores = self._predict_tokens_with_scoring(
+ c_logits, u_logits, gen_config
+ )
+
+ scores = scores - (layer_ids * gen_config.layer_penalty_factor)
+
+ if gen_config.position_temperature > 0.0:
+ scores = _gumbel_sample(scores, gen_config.position_temperature)
+
+ sample_tokens = tokens[i : i + 1, :, :t_len]
+ scores.masked_fill_(
+ sample_tokens != self.config.audio_mask_id, -float("inf")
+ )
+
+ _, topk_idx = torch.topk(scores.flatten(), k)
+ flat_tokens = sample_tokens.flatten()
+ flat_tokens[topk_idx] = pred_tokens.flatten()[topk_idx]
+ sample_tokens.copy_(flat_tokens.view_as(sample_tokens))
+
+ # Update individual slices into batched structure
+ tokens[i : i + 1, :, :t_len] = sample_tokens
+ batch_input_ids[i : i + 1, :, c_len - t_len : c_len] = sample_tokens
+ batch_input_ids[B + i : B + i + 1, :, :t_len] = sample_tokens
+
+ return [tokens[i, :, : task.target_lens[i]] for i in range(B)]
+
+ def _predict_tokens_with_scoring(self, c_logits, u_logits, gen_config):
+ if gen_config.guidance_scale != 0:
+ c_log_probs = F.log_softmax(c_logits, dim=-1)
+ u_log_probs = F.log_softmax(u_logits, dim=-1)
+ log_probs = torch.log_softmax(
+ c_log_probs + gen_config.guidance_scale * (c_log_probs - u_log_probs),
+ dim=-1,
+ )
+ else:
+ log_probs = F.log_softmax(c_logits, dim=-1)
+
+ log_probs[..., self.config.audio_mask_id] = -float("inf")
+
+ if gen_config.class_temperature > 0.0:
+ filtered_probs = _filter_top_k(log_probs, ratio=0.1)
+ pred_tokens = _gumbel_sample(
+ filtered_probs, gen_config.class_temperature
+ ).argmax(dim=-1)
+ else:
+ pred_tokens = log_probs.argmax(dim=-1)
+
+ confidence_scores = log_probs.max(dim=-1)[0]
+
+ return pred_tokens, confidence_scores
+
+
+# ---------------------------------------------------------------------------
+# Standalone helpers
+# ---------------------------------------------------------------------------
+
+
+def _get_packed_mask(document_ids):
+ return partial(_mask_mod_packed, document_ids)
+
+
+def _mask_mod_packed(document_ids, b, h, q_idx, kv_idx):
+ # 1. Sequence Packing Logic: Tokens must belong to the same document.
+ # Note: The doc_id for padding tokens is -1, which will automatically not match
+ # (if handled correctly) or be ignored.
+ same_doc = document_ids[q_idx] == document_ids[kv_idx]
+ return same_doc
+
+
+def _resolve_language(language: Optional[str]) -> Union[str, None]:
+ from omnivoice.utils.lang_map import LANG_IDS, LANG_NAME_TO_ID
+
+ if language is None or language.lower() == "none":
+ return None
+ if language in LANG_IDS:
+ return language
+ key = language.lower()
+ if key in LANG_NAME_TO_ID:
+ return LANG_NAME_TO_ID[key]
+ logger.warning(
+ f"Language '{language}' is not recognized. "
+ f"Please use a valid language ID (e.g., 'en', 'zh', 'ja', 'de') "
+ f"or a full language name (e.g., 'English', 'Chinese', 'Japanese'). "
+ f"See supported_language_ids() or supported_language_names() for details. "
+ f"Falling back to None (language-agnostic mode)."
+ )
+ return None
+
+
+def _resolve_instruct(
+ instruct: Optional[str], use_zh: bool = False
+) -> Union[str, None]:
+ """Validate and normalise a voice-design instruct string.
+
+ Supported instruct items (case-insensitive for English):
+
+ English (comma + space separated):
+ gender: male, female
+ age: child, teenager, young adult, middle-aged, elderly
+ pitch: very low pitch, low pitch, moderate pitch,
+ high pitch, very high pitch
+ style: whisper
+ accent: american accent, british accent, australian accent, ...
+
+ Chinese (full-width comma separated):
+ gender: 男, 女
+ age: 儿童, 少年, 青年, 中年, 老年
+ pitch: 极低音调, 低音调, 中音调, 高音调, 极高音调
+ style: 耳语
+ dialect: 河南话, 陕西话, 四川话, 贵州话, 云南话,
+ 桂林话, 济南话, 石家庄话, 甘肃话, 宁夏话,
+ 青岛话, 东北话
+
+ Minor issues (auto-fixed):
+ - Wrong separator (half-width comma in Chinese instruct or
+ full-width comma in English instruct)
+ - Leading / trailing commas
+
+ Major issues (raise ``ValueError``):
+ - Unsupported or misspelled instruct items
+ - Suggestions are offered for close matches
+
+ Args:
+ instruct: Raw instruct string, or ``None``.
+ use_zh: If True, normalise all items to Chinese (used when the
+ synthesis text contains Chinese and no accent is specified).
+
+ Returns:
+ Normalised instruct string, or ``None``.
+
+ Raises:
+ ValueError: if any instruct item is unsupported or misspelled.
+ """
+ if instruct is None:
+ return None
+
+ instruct_str = instruct.strip()
+ if not instruct_str:
+ return None
+
+ # Split on both half-width and full-width commas
+ raw_items = re.split(r"\s*[,,]\s*", instruct_str)
+ raw_items = [x for x in raw_items if x]
+
+ # Validate each item
+ unknown = []
+ normalised = []
+ for raw in raw_items:
+ n = raw.strip().lower()
+ if n in _INSTRUCT_ALL_VALID:
+ normalised.append(n)
+ else:
+ sug = difflib.get_close_matches(n, _INSTRUCT_ALL_VALID, n=1, cutoff=0.6)
+ unknown.append((raw, n, sug[0] if sug else None))
+
+ if unknown:
+ lines = []
+ for raw, n, sug in unknown:
+ if sug:
+ lines.append(f" '{raw}' -> '{n}' (unsupported; did you mean '{sug}'?)")
+ else:
+ lines.append(f" '{raw}' -> '{n}' (unsupported)")
+ err = (
+ f"Unsupported instruct items found in {instruct_str}:\n"
+ + "\n".join(lines)
+ + "\n\nValid English items: "
+ + ", ".join(sorted(_INSTRUCT_VALID_EN))
+ + "\nValid Chinese items: "
+ + ",".join(sorted(_INSTRUCT_VALID_ZH))
+ + "\n\nTip: Use only English or only Chinese instructs. "
+ "English instructs should use comma + space (e.g. "
+ "'male, indian accent'),\nChinese instructs should use full-width "
+ "comma (e.g. '男,河南话')."
+ )
+ raise ValueError(err)
+
+ # --- Language consistency: dialect forces Chinese, accent forces English ---
+ has_dialect = any(n.endswith("话") for n in normalised)
+ has_accent = any(" accent" in n for n in normalised)
+
+ if has_dialect and has_accent:
+ raise ValueError(
+ "Cannot mix Chinese dialect and English accent in a single instruct. "
+ "Dialects are for Chinese speech, accents for English speech."
+ )
+
+ if has_dialect:
+ use_zh = True
+ elif has_accent:
+ use_zh = False
+
+ # --- Unify to single language ---
+ if use_zh:
+ normalised = [_INSTRUCT_EN_TO_ZH.get(n, n) for n in normalised]
+ else:
+ normalised = [_INSTRUCT_ZH_TO_EN.get(n, n) for n in normalised]
+
+ # --- Category conflict check ---
+ conflicts = []
+ for cat in _INSTRUCT_MUTUALLY_EXCLUSIVE:
+ hits = [n for n in normalised if n in cat]
+ if len(hits) > 1:
+ conflicts.append(hits)
+ if conflicts:
+ parts = []
+ for group in conflicts:
+ parts.append(" vs ".join(f"'{x}'" for x in group))
+ raise ValueError(
+ "Conflicting instruct items within the same category: "
+ + "; ".join(parts)
+ + ". Each category (gender, age, pitch, style, accent, dialect) "
+ "allows at most one item."
+ )
+
+ # Determine separator based on language
+ has_zh = any(any("\u4e00" <= c <= "\u9fff" for c in n) for n in normalised)
+ separator = "," if has_zh else ", "
+
+ return separator.join(normalised)
+
+
+def _filter_top_k(logits: torch.Tensor, ratio: float = 0.1) -> torch.Tensor:
+ k = math.ceil(ratio * logits.shape[-1])
+ val, ind = logits.topk(k, dim=-1)
+ probs = torch.full_like(logits, float("-inf"))
+ probs.scatter_(-1, ind, val)
+ return probs
+
+
+def _gumbel_sample(logits: torch.Tensor, temperature: float) -> torch.Tensor:
+ scaled_logits = logits / temperature
+ u = torch.rand_like(scaled_logits)
+ gumbel_noise = -torch.log(-torch.log(u + 1e-10) + 1e-10)
+ return scaled_logits + gumbel_noise
+
+
+def _get_time_steps(
+ t_start: float = 0.0,
+ t_end: float = 1.0,
+ num_step: int = 10,
+ t_shift: float = 1.0,
+ device: torch.device = torch.device("cpu"),
+) -> torch.Tensor:
+ timesteps = torch.linspace(t_start, t_end, num_step + 1).to(device)
+ timesteps = t_shift * timesteps / (1 + (t_shift - 1) * timesteps)
+ return timesteps
+
+
+_NONVERBAL_PATTERN = re.compile(
+ r"\[(laughter|sigh|confirmation-en|question-en|question-ah|question-oh|"
+ r"question-ei|question-yi|surprise-ah|surprise-oh|surprise-wa|"
+ r"surprise-yo|dissatisfaction-hnn)\]"
+)
+
+
+def _tokenize_with_nonverbal_tags(text: str, tokenizer) -> torch.Tensor:
+ """Tokenize text containing non-verbal tags, handling each tag independently.
+
+ Non-verbal tags are tokenized standalone to guarantee consistent token
+ IDs regardless of surrounding language context (Chinese, English, etc.).
+
+ Args:
+ text: Full text string potentially containing non-verbal tags.
+ tokenizer: HuggingFace text tokenizer instance.
+ Returns:
+ Token IDs tensor of shape (1, seq_len).
+ """
+ parts = []
+ last_end = 0
+ for m in _NONVERBAL_PATTERN.finditer(text):
+ if m.start() > last_end:
+ segment = text[last_end : m.start()]
+ ids = tokenizer(segment, add_special_tokens=False).input_ids
+ if ids:
+ parts.append(ids)
+ tag_ids = tokenizer(m.group(), add_special_tokens=False).input_ids
+ if tag_ids:
+ parts.append(tag_ids)
+ last_end = m.end()
+ if last_end < len(text):
+ segment = text[last_end:]
+ ids = tokenizer(segment, add_special_tokens=False).input_ids
+ if ids:
+ parts.append(ids)
+
+ if not parts:
+ result = tokenizer(text, return_tensors="pt").input_ids
+ else:
+ combined = []
+ for p in parts:
+ combined.extend(p)
+ result = torch.tensor([combined], dtype=torch.long)
+ return result
+
+
+def _combine_text(text, ref_text: Optional[str] = None) -> str:
+
+ # combine with reference text if not None
+ if ref_text:
+ full_text = ref_text.strip() + " " + text.strip()
+ else:
+ full_text = text.strip()
+
+ # filter out newline / carriage-return characters
+ full_text = re.sub(r"[\r\n]+", "", full_text)
+
+ # collapse consecutive spaces / tabs into a single space
+ full_text = re.sub(r"[ \t]+", " ", full_text)
+
+ # remove spaces around chinese characters
+ chinese_range = r"[\u4e00-\u9fff]"
+ pattern = rf"(?<={chinese_range})\s+|\s+(?={chinese_range})"
+ full_text = re.sub(pattern, "", full_text)
+
+ return full_text
+
+
+# ---------------------------------------------------------------------------
+# Register with HuggingFace Auto classes
+# ---------------------------------------------------------------------------
+
+AutoConfig.register("omnivoice", OmniVoiceConfig)
+AutoModel.register(OmniVoiceConfig, OmniVoice)
diff --git a/omnivoice/scripts/__init__.py b/omnivoice/scripts/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/scripts/denoise_audio.py b/omnivoice/scripts/denoise_audio.py
new file mode 100644
index 00000000..a7900a7f
--- /dev/null
+++ b/omnivoice/scripts/denoise_audio.py
@@ -0,0 +1,1048 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Denoise audio with Sidon and pack results into WebDataset shards.
+
+Supports two input modes:
+
+1. WebDataset manifest (data.lst):
+ python denoise_audio.py \
+ --input_manifest data.lst \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl \
+ --feature_extractor_path sidon-v0.1/feature_extractor_cuda.pt \
+ --decoder_path sidon-v0.1/decoder_cuda.pt
+
+2. Raw JSONL (each line: {"id": "...", "audio_path": "...", ...}):
+ python denoise_audio.py \
+ --input_jsonl data.jsonl \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl \
+ --feature_extractor_path sidon-v0.1/feature_extractor_cuda.pt \
+ --decoder_path sidon-v0.1/decoder_cuda.pt
+
+Output structure:
+ output_dir/
+ ├── audios/ # WebDataset tar shards (.flac audio + .json metadata)
+ │ ├── shard_000000.tar
+ │ └── ...
+ ├── txts/ # Per-shard JSONL metadata
+ │ ├── shard_000000.jsonl
+ │ └── ...
+ ├── data.lst # Manifest:
+ └── errors.jsonl # Failed samples with error details
+"""
+
+from __future__ import annotations
+
+import argparse
+import io
+import json
+import logging
+import os
+import pickle
+import struct
+import subprocess
+import sys
+import threading
+from concurrent.futures import FIRST_COMPLETED, Future, wait
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Sequence, Union
+
+import numpy as np
+import torch
+import torchaudio
+import webdataset as wds
+from torch.utils.data import DataLoader
+from tqdm.auto import tqdm
+
+from omnivoice.data.batching import StreamLengthGroupDataset
+from omnivoice.data.dataset import JsonlDatasetReader, WebDatasetReader
+from omnivoice.utils.common import str2bool
+
+SIDON_INPUT_SAMPLE_RATE = 16_000
+SIDON_OUTPUT_SAMPLE_RATE = 48_000
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+
+ # ── Input (mutually exclusive) ──
+ parser.add_argument(
+ "--input_manifest",
+ default=None,
+ help="WebDataset manifest (data.lst). Each line: "
+ " ",
+ )
+ parser.add_argument(
+ "--input_jsonl",
+ default=None,
+ help="Raw JSONL file. Each line: " '{"id": "...", "audio_path": "...", ...}',
+ )
+
+ # ── Output ──
+ parser.add_argument(
+ "--tar_output_pattern",
+ default=None,
+ help="Tar shard pattern, e.g. output/audios/shard_%%06d.tar",
+ )
+ parser.add_argument(
+ "--jsonl_output_pattern",
+ default=None,
+ help="JSONL shard pattern, e.g. output/txts/shard_%%06d.jsonl",
+ )
+ parser.add_argument(
+ "--samples_per_shard",
+ type=int,
+ default=1_000,
+ help="Maximum records per output shard",
+ )
+
+ # ── Model ──
+ parser.add_argument(
+ "--feature_extractor_path",
+ default=None,
+ help="Path to feature_extractor_cuda.pt",
+ )
+ parser.add_argument(
+ "--decoder_path",
+ default=None,
+ help="Path to decoder_cuda.pt",
+ )
+ parser.add_argument(
+ "--target_sample_rate",
+ type=int,
+ default=24_000,
+ help="Sample rate of the denoised output audio",
+ )
+
+ # ── Filtering ──
+ parser.add_argument(
+ "--min_length",
+ type=float,
+ default=0.0,
+ help="Minimum audio duration in seconds",
+ )
+ parser.add_argument(
+ "--max_length",
+ type=float,
+ default=80.0,
+ help="Maximum audio duration in seconds",
+ )
+
+ # ── Batching ──
+ parser.add_argument(
+ "--batch_duration",
+ type=float,
+ default=200.0,
+ help="Target batch duration in seconds for dynamic batching",
+ )
+ parser.add_argument(
+ "--max_sample",
+ type=int,
+ default=32,
+ help="Maximum samples per batch for dynamic batching",
+ )
+
+ # ── Distributed ──
+ parser.add_argument(
+ "--num_machines",
+ type=int,
+ default=1,
+ help="Total number of machines for distributed runs",
+ )
+ parser.add_argument(
+ "--machine_index",
+ type=int,
+ default=0,
+ help="Zero-based machine index when distributing across multiple "
+ "machines (e.g. 0, 1, ... num_machines-1)",
+ )
+
+ # ── Parallelism ──
+ parser.add_argument(
+ "--nj_per_gpu",
+ type=int,
+ default=1,
+ help="Worker processes per GPU (default 1)",
+ )
+ parser.add_argument(
+ "--loader_workers",
+ type=int,
+ default=16,
+ help="PyTorch DataLoader worker threads",
+ )
+
+ # ── Data order (JSONL mode) ──
+ parser.add_argument(
+ "--shuffle",
+ type=str2bool,
+ default=True,
+ help="Shuffle JSONL entries",
+ )
+ parser.add_argument(
+ "--shuffle_seed",
+ type=int,
+ default=42,
+ help="Seed for JSONL shuffle",
+ )
+
+ # ── Error handling ──
+ parser.add_argument(
+ "--skip_errors",
+ action="store_true",
+ help="Skip items that fail to denoise instead of aborting",
+ )
+ parser.add_argument(
+ "--_subprocess_worker",
+ action="store_true",
+ help=argparse.SUPPRESS,
+ )
+ return parser
+
+
+# ---------------------------------------------------------------------------
+# Utilities
+# ---------------------------------------------------------------------------
+
+
+def count_lines(path: str) -> int:
+ """Count newlines efficiently by reading binary chunks."""
+ count = 0
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(1 << 20), b""):
+ count += chunk.count(b"\n")
+ return count
+
+
+PaddingStrategy = Union[bool, str]
+ReturnType = Union[torch.Tensor, np.ndarray]
+
+
+def extract_seamless_m4t_features(
+ raw_speech: Union[torch.Tensor, List[float], List[torch.Tensor], List[List[float]]],
+ sampling_rate: int = 16000,
+ num_mel_bins: int = 80,
+ frame_length: int = 25,
+ frame_shift: int = 10,
+ preemphasis_coefficient: float = 0.97,
+ dither: float = 0.0,
+ window_type: str = "povey",
+ do_normalize_per_mel_bins: bool = True,
+ stride: int = 2,
+ padding: PaddingStrategy = "longest",
+ max_length: Optional[int] = None,
+ pad_to_multiple_of: Optional[int] = 2,
+ return_tensors: Optional[str] = "pt",
+ return_attention_mask: bool = True,
+ padding_value: float = 0.0,
+ device: torch.device = torch.device("cpu"),
+) -> Dict[str, ReturnType]:
+ """Extract SeamlessM4T features using Torch-only operators."""
+ if not isinstance(raw_speech, list):
+ raw_speech = [raw_speech]
+
+ processed_speech = [
+ torch.as_tensor(sample, dtype=torch.float32, device=device)
+ for sample in raw_speech
+ ]
+
+ features: List[torch.Tensor] = []
+ for waveform in processed_speech:
+ if waveform.ndim > 1:
+ waveform = waveform[0]
+ waveform_tensor = waveform.unsqueeze(0)
+ feature = torchaudio.compliance.kaldi.fbank(
+ waveform=waveform_tensor,
+ sample_frequency=sampling_rate,
+ num_mel_bins=num_mel_bins,
+ frame_length=frame_length,
+ frame_shift=frame_shift,
+ dither=dither,
+ preemphasis_coefficient=preemphasis_coefficient,
+ remove_dc_offset=True,
+ window_type=window_type,
+ use_energy=False,
+ energy_floor=1.192092955078125e-07,
+ )
+ features.append(feature.squeeze(0))
+
+ if do_normalize_per_mel_bins:
+ normalised: List[torch.Tensor] = []
+ for feature in features:
+ mean = feature.mean(0, keepdim=True)
+ var = feature.var(0, keepdim=True)
+ normalised.append((feature - mean) / torch.sqrt(var + 1e-5))
+ features = normalised
+
+ def _pad_batch(
+ features: List[torch.Tensor],
+ padding_strategy: PaddingStrategy = "longest",
+ max_length: Optional[int] = None,
+ pad_to_multiple_of: Optional[int] = None,
+ padding_value: float = 0.0,
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ if padding_strategy == "longest":
+ target_length = max(f.shape[0] for f in features)
+ elif max_length is not None:
+ target_length = max_length
+ else:
+ raise ValueError(
+ "max_length must be provided when padding_strategy is not 'longest'"
+ )
+
+ if pad_to_multiple_of is not None:
+ target_length = (
+ (target_length + pad_to_multiple_of - 1)
+ // pad_to_multiple_of
+ * pad_to_multiple_of
+ )
+
+ batch_size = len(features)
+ feature_dim = features[0].shape[1]
+ device = features[0].device
+
+ padded_features = torch.full(
+ (batch_size, target_length, feature_dim),
+ padding_value,
+ dtype=torch.float32,
+ device=device,
+ )
+ attention_mask = torch.zeros(
+ (batch_size, target_length),
+ dtype=torch.int64,
+ device=device,
+ )
+
+ for index, feature_tensor in enumerate(features):
+ seq_len = feature_tensor.shape[0]
+ padded_features[index, :seq_len] = feature_tensor
+ attention_mask[index, :seq_len] = 1
+
+ return padded_features, attention_mask
+
+ input_features, attention_mask = _pad_batch(
+ features,
+ padding_strategy=padding,
+ max_length=max_length,
+ pad_to_multiple_of=pad_to_multiple_of,
+ padding_value=padding_value,
+ )
+
+ batch_size, num_frames, num_channels = input_features.shape
+ new_num_frames = (num_frames // stride) * stride
+ input_features = input_features[:, :new_num_frames, :]
+ if return_attention_mask:
+ attention_mask = attention_mask[:, :new_num_frames]
+
+ input_features = input_features.reshape(
+ batch_size, new_num_frames // stride, num_channels * stride
+ )
+
+ output: Dict[str, ReturnType] = {"input_features": input_features}
+ if return_attention_mask:
+ output["attention_mask"] = attention_mask[:, 1::stride]
+
+ if return_tensors == "np":
+ for key, value in output.items():
+ output[key] = value.cpu().numpy() # type: ignore[assignment]
+
+ return output
+
+
+def serialise_flac(key: str, waveform: torch.Tensor, sample_rate: int) -> dict:
+ buffer = io.BytesIO()
+ audio = waveform.to(dtype=torch.float32).cpu()
+ if audio.ndim == 1:
+ audio = audio.unsqueeze(0)
+ torchaudio.save(buffer, audio, sample_rate, format="flac", bits_per_sample=16)
+ return {"__key__": key, "flac": buffer.getvalue()}
+
+
+def _normalise_value(value: Any) -> Any:
+ """Convert tensors and NumPy scalars to serialisable Python objects."""
+ if isinstance(value, torch.Tensor):
+ if value.ndim == 0:
+ return value.item()
+ return value.cpu().tolist()
+ if isinstance(value, np.generic):
+ return value.item()
+ if isinstance(value, np.ndarray):
+ return value.tolist()
+ return value
+
+
+def _encode_metadata(metadata: dict[str, Any]) -> bytes:
+ cleaned: dict[str, Any] = {}
+ for key, value in metadata.items():
+ if value is None:
+ continue
+ cleaned[key] = _normalise_value(value)
+ return json.dumps(cleaned, ensure_ascii=False).encode("utf-8")
+
+
+# ---------------------------------------------------------------------------
+# Denoising model
+# ---------------------------------------------------------------------------
+
+
+class SpeechDenoisingProcessor:
+ """Run the TorchScripted feature extractor and decoder."""
+
+ def __init__(
+ self,
+ feature_extractor_path: str,
+ decoder_path: str,
+ device: str,
+ ) -> None:
+ self.device = torch.device(device)
+ self.feature_extractor = torch.jit.load(
+ feature_extractor_path, map_location=self.device
+ )
+ self.decoder = torch.jit.load(decoder_path, map_location=self.device)
+ self.feature_extractor.eval()
+ self.decoder.eval()
+
+ @torch.inference_mode()
+ def process(self, waveform: torch.Tensor, sample_rate: int) -> torch.Tensor:
+ return self.process_batch([waveform], [sample_rate])[0]
+
+ @torch.inference_mode()
+ def process_batch(
+ self,
+ waveforms: Sequence[torch.Tensor] | torch.Tensor,
+ sample_rates: Optional[Sequence[int]] = None,
+ expected_lengths: Optional[Sequence[int]] = None,
+ ) -> List[torch.Tensor]:
+ if expected_lengths is None:
+ expected_lengths: list[int] = []
+ for waveform, sample_rate in zip(waveforms, sample_rates):
+ duration_seconds = waveform.shape[-1] / float(sample_rate)
+ expected_lengths.append(
+ int(round(duration_seconds * SIDON_OUTPUT_SAMPLE_RATE))
+ )
+ waveforms = torch.nn.functional.pad(waveforms, (0, 24000))
+
+ features = extract_seamless_m4t_features(
+ [x for x in waveforms],
+ return_tensors="pt",
+ padding_value=1.0,
+ device=self.device,
+ )
+ feature_tensor = self.feature_extractor(
+ features["input_features"].to(self.device)
+ )["last_hidden_state"]
+ restored_waveforms = self.decoder(feature_tensor.transpose(1, 2)).cpu()
+
+ results: List[torch.Tensor] = []
+ for sample_idx, sample in enumerate(restored_waveforms):
+ restored_waveform = sample.view(-1)
+ target_length = expected_lengths[sample_idx]
+ current_length = restored_waveform.shape[-1]
+ if target_length > 0 and current_length != target_length:
+ diff = target_length - current_length
+ if diff > 0:
+ restored_waveform = torch.nn.functional.pad(
+ restored_waveform, (0, diff)
+ )
+ elif diff < 0:
+ restored_waveform = restored_waveform[:target_length]
+ results.append(restored_waveform.contiguous())
+
+ return results
+
+
+# ---------------------------------------------------------------------------
+# Batch collation
+# ---------------------------------------------------------------------------
+
+
+class CollateFunction:
+ """Collate a list of samples into a padded batch."""
+
+ def __init__(
+ self,
+ sample_rate: int,
+ skip_errors: bool,
+ ) -> None:
+ self.sample_rate = sample_rate
+ self.skip_errors = skip_errors
+
+ def __call__(self, samples: Sequence[dict[str, Any]]) -> CollatedBatch:
+ keys: list[str] = []
+ waveforms: list[torch.Tensor] = []
+ durations: list[float] = []
+ metadata: list[dict[str, Any]] = []
+
+ for sample in samples:
+ keys.append(sample["label"]["id"])
+ waveforms.append(sample["audio"].squeeze(0))
+ durations.append(sample["audio"].size(-1) / self.sample_rate)
+ metadata.append(sample["label"])
+ waveforms = torch.nn.utils.rnn.pad_sequence(waveforms, batch_first=True)
+
+ return CollatedBatch(
+ keys=keys, waveforms=waveforms, durations=durations, metadata=metadata
+ )
+
+
+@dataclass
+class CollatedBatch:
+ """Batch payload returned by the DataLoader collate function."""
+
+ keys: list[str]
+ waveforms: list[torch.Tensor]
+ durations: list[float]
+ metadata: list[dict[str, Any]]
+
+ @property
+ def size(self) -> int:
+ return len(self.keys)
+
+
+# ---------------------------------------------------------------------------
+# Subprocess-based GPU worker pool
+# ---------------------------------------------------------------------------
+#
+# Problem: PyTorch ≥2.8 caches CUDA device state at import time. Neither
+# forkserver nor spawn lets us change CUDA_VISIBLE_DEVICES *before* the CUDA
+# runtime captures the device list. The only reliable approach is to launch
+# each worker as a **subprocess** with CUDA_VISIBLE_DEVICES set in the
+# subprocess environment, guaranteeing it takes effect before `import torch`.
+#
+# Protocol (parent ↔ child, length-prefixed pickle over stdin/stdout):
+# Parent → child: 4-byte LE uint32 length + pickle(CollatedBatch)
+# Child → parent: 4-byte LE uint32 length + pickle(result dict)
+# Shutdown signal: 4 zero bytes (length == 0)
+
+
+def _subprocess_recv():
+ """Read a length-prefixed pickled object from stdin. Returns None on shutdown."""
+ raw = sys.stdin.buffer.read(4)
+ if len(raw) < 4:
+ return None
+ (length,) = struct.unpack(" Future:
+ worker = self.workers[self._rr % len(self.workers)]
+ self._rr += 1
+ with self._futures_lock:
+ req_id = self._next_id
+ self._next_id += 1
+ fut = Future()
+ self._futures[req_id] = fut
+ batch_dict = {
+ "_req_id": req_id,
+ "_batch": batch,
+ }
+ worker.submit(batch_dict)
+ return fut
+
+ def shutdown(self):
+ for worker in self.workers:
+ worker.shutdown()
+ for t in self._reader_threads:
+ t.join(timeout=5)
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+
+def main() -> None:
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+ parser = build_parser()
+ args = parser.parse_args()
+
+ # ── Subprocess worker mode ──
+ if args._subprocess_worker:
+ subprocess_worker_main()
+ return
+
+ # Validate input arguments
+ assert args.tar_output_pattern is not None, "--tar_output_pattern is required."
+ assert args.jsonl_output_pattern is not None, "--jsonl_output_pattern is required."
+ assert bool(args.input_manifest) != bool(
+ args.input_jsonl
+ ), "Exactly one of --input_manifest or --input_jsonl must be provided."
+
+ if args.num_machines > 1:
+ assert (
+ 0 <= args.machine_index < args.num_machines
+ ), f"machine_index {args.machine_index} must be in [0, {args.num_machines})"
+
+ # ── Build base dataset and count total samples ──
+ if args.input_jsonl:
+ logging.info(f"Input mode: raw JSONL ({args.input_jsonl})")
+ total_samples = count_lines(args.input_jsonl)
+ base_dataset = JsonlDatasetReader(
+ args.input_jsonl,
+ sample_rate=SIDON_INPUT_SAMPLE_RATE,
+ shuffle=args.shuffle,
+ shuffle_seed=args.shuffle_seed,
+ )
+ loader_workers = args.loader_workers
+ else:
+ logging.info(f"Input mode: WebDataset manifest ({args.input_manifest})")
+ manifest_num_lines = count_lines(args.input_manifest)
+ loader_workers = min(args.loader_workers, manifest_num_lines)
+ total_samples = 0
+ manifests = []
+ with open(args.input_manifest, "r", encoding="utf-8") as f:
+ for line_id, line in tqdm(
+ enumerate(f),
+ total=manifest_num_lines,
+ desc="Calculating dataset length",
+ ):
+ items = line.strip().split(" ")
+ tar_path, jsonl_path, num_items, duration = (
+ items[0],
+ items[1],
+ int(items[2]),
+ float(items[3]),
+ )
+ assert os.path.exists(tar_path), f"File {tar_path} does not exist."
+ assert os.path.exists(jsonl_path), f"File {jsonl_path} does not exist."
+ assert jsonl_path.endswith(
+ ".jsonl"
+ ), f"File {jsonl_path} is not a .jsonl file."
+ if (
+ args.num_machines > 1
+ and line_id % args.num_machines != args.machine_index
+ ):
+ continue
+ total_samples += num_items
+ manifests.append((tar_path, jsonl_path, num_items, duration))
+ logging.info(
+ f"Total shards: {manifest_num_lines}, "
+ f"Shards for current index: {len(manifests)}"
+ )
+ base_dataset = WebDatasetReader(
+ manifests=manifests,
+ sample_rate=SIDON_INPUT_SAMPLE_RATE,
+ evaluation=True,
+ )
+
+ # ── Dynamic batching + DataLoader ──
+ batched_dataset = StreamLengthGroupDataset(
+ dataset=base_dataset,
+ batch_duration=args.batch_duration,
+ max_sample=args.max_sample,
+ min_length=args.min_length,
+ max_length=args.max_length,
+ )
+
+ collate_fn = CollateFunction(
+ skip_errors=args.skip_errors,
+ sample_rate=SIDON_INPUT_SAMPLE_RATE,
+ )
+
+ dataloader = DataLoader(
+ dataset=batched_dataset,
+ batch_size=None,
+ collate_fn=collate_fn,
+ num_workers=loader_workers,
+ prefetch_factor=10 if loader_workers > 0 else None,
+ pin_memory=True,
+ persistent_workers=loader_workers > 0,
+ )
+
+ # ── Multi-GPU process pool ──
+ num_devices = torch.cuda.device_count()
+ if num_devices == 0:
+ logging.warning("No GPUs detected - using CPU for processing")
+ num_processes = args.nj_per_gpu
+ else:
+ num_processes = num_devices * args.nj_per_gpu
+ logging.info(
+ f"GPU count: {num_devices}, Processes per GPU: {args.nj_per_gpu}, "
+ f"Total processes: {num_processes}"
+ )
+
+ # Build a list of (physical_gpu_id, num_workers) for each pool.
+ # When num_devices == 0 we use a single CPU pool.
+ if num_devices == 0:
+ pool_specs = [(None, num_processes)]
+ else:
+ pool_specs = [(gpu_id, args.nj_per_gpu) for gpu_id in range(num_devices)]
+
+ # ── Output paths ──
+ tar_output_pattern = str(Path(args.tar_output_pattern).expanduser())
+ jsonl_output_pattern = str(Path(args.jsonl_output_pattern).expanduser())
+ Path(tar_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+ Path(jsonl_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+
+ output_dir = Path(tar_output_pattern).parent.parent
+ error_log_path = str(output_dir / "errors.jsonl")
+ manifest_path = str(output_dir / "data.lst")
+
+ error_logger = logging.getLogger("error_log")
+ error_logger.setLevel(logging.ERROR)
+ error_logger.handlers.clear()
+ error_fh = logging.FileHandler(error_log_path, mode="w", encoding="utf-8")
+ error_fh.setFormatter(logging.Formatter("%(message)s"))
+ error_logger.addHandler(error_fh)
+
+ # ── Progress and shard tracking ──
+ processed_count = 0
+ error_count = 0
+ write_error_count = 0
+ failed_ids = []
+ shard_idx = 0
+ shard_sample_count = 0
+ shard_duration = 0.0
+ samples_per_shard = args.samples_per_shard
+ shard_manifest = {}
+ target_sample_rate = args.target_sample_rate
+
+ tar_writer = None
+ jsonl_file = None
+
+ def open_new_shard():
+ nonlocal tar_writer, jsonl_file, shard_idx, shard_sample_count, shard_duration
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ if shard_idx > 0 and shard_sample_count > 0:
+ prev_idx = shard_idx - 1
+ shard_manifest[prev_idx] = (
+ os.path.abspath(tar_output_pattern % prev_idx),
+ os.path.abspath(jsonl_output_pattern % prev_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+ tar_fname = tar_output_pattern % shard_idx
+ jsonl_fname = jsonl_output_pattern % shard_idx
+ tar_writer = wds.TarWriter(tar_fname)
+ jsonl_file = open(jsonl_fname, "w", encoding="utf-8")
+ shard_idx += 1
+ shard_sample_count = 0
+ shard_duration = 0.0
+
+ def write_sample(key, waveform, metadata):
+ nonlocal shard_sample_count, write_error_count, shard_duration
+ assert tar_writer is not None and jsonl_file is not None
+ try:
+ if target_sample_rate != SIDON_OUTPUT_SAMPLE_RATE:
+ waveform = torchaudio.functional.resample(
+ waveform,
+ orig_freq=SIDON_OUTPUT_SAMPLE_RATE,
+ new_freq=target_sample_rate,
+ )
+ waveform = (waveform / (waveform.abs().max() + 1e-7)) * 0.6
+
+ record = serialise_flac(key, waveform, target_sample_rate)
+ jsonl_record = _encode_metadata(metadata)
+ tar_writer.write(record)
+ jsonl_file.write(jsonl_record.decode("utf-8") + "\n")
+ shard_sample_count += 1
+ shard_duration += metadata.get("audio_duration", 0.0)
+ except Exception as exc:
+ write_error_count += 1
+ failed_ids.append(key)
+ error_logger.error(
+ json.dumps({"id": key, "reason": str(exc)}, ensure_ascii=False)
+ )
+ logging.error(f"Write failed for sample {key}: {exc}")
+
+ def handle_result(result):
+ nonlocal processed_count, error_count
+ if result["status"] == "success":
+ for key, cleaned, metadata in zip(
+ result["keys"], result["results"], result["metadata"]
+ ):
+ if tar_writer is None or shard_sample_count >= samples_per_shard:
+ open_new_shard()
+ write_sample(key, cleaned, metadata)
+ processed_count += 1
+ else:
+ error_count += result["size"]
+ failed_ids.extend(result["keys"])
+ for key in result["keys"]:
+ error_logger.error(
+ json.dumps(
+ {"id": key, "reason": result["error"]},
+ ensure_ascii=False,
+ )
+ )
+ if not args.skip_errors:
+ raise RuntimeError(
+ f"Batch starting with {result['keys'][0]} failed - terminating"
+ )
+ logging.warning(
+ f"Skipping failed batch starting with {result['keys'][0]}: "
+ f"{result['error']}"
+ )
+
+ # ── Main processing loop ──
+ main_progress = tqdm(total=total_samples, desc="Denoising Audio")
+
+ # Launch subprocess-based GPU workers. CUDA_VISIBLE_DEVICES is set in the
+ # subprocess Popen environment so it takes effect before import torch.
+ pool = GPUWorkerPool(pool_specs, args.feature_extractor_path, args.decoder_path)
+ logging.info(f"Submitting tasks... ({num_processes} subprocess workers)")
+ try:
+ futures = set()
+ max_pending = num_processes * 2
+
+ def drain_completed():
+ nonlocal futures
+ done, _ = wait(futures, return_when=FIRST_COMPLETED)
+ for f in done:
+ futures.discard(f)
+ result = f.result()
+ main_progress.update(result["size"])
+ handle_result(result)
+ main_progress.set_postfix(
+ OK=processed_count,
+ Err=error_count,
+ )
+
+ for batch in dataloader:
+ if batch.size == 0:
+ continue
+ if len(futures) >= max_pending:
+ drain_completed()
+ futures.add(pool.submit(batch))
+
+ logging.info("Processing remaining pending batches...")
+ while futures:
+ drain_completed()
+
+ except Exception:
+ logging.error("Critical error during processing", exc_info=True)
+ raise
+ finally:
+ pool.shutdown()
+ main_progress.close()
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ if shard_idx > 0 and shard_sample_count > 0:
+ last_idx = shard_idx - 1
+ shard_manifest[last_idx] = (
+ os.path.abspath(tar_output_pattern % last_idx),
+ os.path.abspath(jsonl_output_pattern % last_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+
+ # ── Write manifest (data.lst) ──
+ with open(manifest_path, "w", encoding="utf-8") as mf:
+ for idx in sorted(shard_manifest.keys()):
+ tar_path, jsonl_path, count, duration = shard_manifest[idx]
+ mf.write(f"{tar_path} {jsonl_path} {count} {duration:.3f}\n")
+
+ # ── Summary ──
+ total_failed = error_count + write_error_count
+ filtered_and_skipped = total_samples - processed_count - total_failed
+ logging.info(
+ f"Processing Complete - Successful: {processed_count}, Failed: {total_failed}, "
+ f"Filtered/Skipped: {filtered_and_skipped}, Shards written: {shard_idx}"
+ )
+ logging.info(f"Manifest written to: {manifest_path} ({len(shard_manifest)} shards)")
+ if total_failed > 0:
+ logging.info(f"Error details: {error_log_path}")
+ if failed_ids and args.skip_errors:
+ logging.warning(
+ f"Failed sample IDs (count: {len(failed_ids)}): {failed_ids[:100]}..."
+ )
+ if write_error_count > 0 and not args.skip_errors:
+ raise RuntimeError(
+ f"{write_error_count} samples failed to write - check logs for details"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/scripts/extract_audio_tokens.py b/omnivoice/scripts/extract_audio_tokens.py
new file mode 100644
index 00000000..2f03a4fc
--- /dev/null
+++ b/omnivoice/scripts/extract_audio_tokens.py
@@ -0,0 +1,625 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Extract audio tokens from audio data and pack them into WebDataset shards.
+
+Supports two input modes:
+
+1. WebDataset manifest (data.lst):
+ python extract_audio_tokens.py \
+ --input_manifest data.lst \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl
+
+2. Raw JSONL (each line: {"id": "...", "audio_path": "...", "text": "...", ...}):
+ python extract_audio_tokens.py \
+ --input_jsonl data.jsonl \
+ --tar_output_pattern output/audios/shard-%06d.tar \
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl
+
+Output structure:
+ output_dir/
+ ├── audios/ # WebDataset tar shards (.npy audio tokens + .json metadata)
+ │ ├── shard_000000.tar
+ │ └── ...
+ ├── txts/ # Per-shard JSONL metadata
+ │ ├── shard_000000.jsonl
+ │ └── ...
+ ├── data.lst # Manifest:
+ └── errors.jsonl # Failed samples with error details
+"""
+
+import argparse
+import io
+import json
+import logging
+import multiprocessing as mp
+import os
+import warnings
+from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait
+from pathlib import Path
+from typing import Any
+
+import numpy as np
+import torch
+import webdataset as wds
+from torch.utils.data import DataLoader, IterableDataset
+from tqdm.auto import tqdm
+from transformers import AutoFeatureExtractor, HiggsAudioV2TokenizerModel
+
+from omnivoice.data.dataset import JsonlDatasetReader, WebDatasetReader
+from omnivoice.utils.common import str2bool
+
+warnings.filterwarnings(
+ "ignore", category=FutureWarning, module="torch.nn.utils.weight_norm"
+)
+
+HIGGS_INPUT_SAMPLE_RATE = 24_000
+
+
+# Global variables: Store tokenizer and device for each worker process
+worker_tokenizer = None
+worker_feature_extractor = None
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--input_manifest",
+ default=None,
+ help="Path to input dataset manifest (data.lst).",
+ )
+ parser.add_argument(
+ "--input_jsonl",
+ default=None,
+ help="Path to raw JSONL file (alternative to --input_manifest).",
+ )
+ parser.add_argument(
+ "--tar_output_pattern",
+ required=True,
+ help="Tar shard pattern passed to WebDataset",
+ )
+ parser.add_argument(
+ "--jsonl_output_pattern",
+ required=True,
+ help="Jsonl shard pattern passed to WebDataset",
+ )
+ parser.add_argument(
+ "--samples_per_shard",
+ type=int,
+ default=1000,
+ help="Maximum records per shard",
+ )
+ parser.add_argument(
+ "--min_num_shards",
+ type=int,
+ default=32,
+ help="Minimum number of output shards (use to ensure "
+ "shard count >= num_gpu * num_workers)",
+ )
+ parser.add_argument(
+ "--tokenizer_path",
+ type=str,
+ default="eustlb/higgs-audio-v2-tokenizer",
+ help="Path to audio tokenizer.",
+ )
+ parser.add_argument(
+ "--skip_errors", action="store_true", help="Skip items that fail to process"
+ )
+ parser.add_argument(
+ "--min_length",
+ type=float,
+ default=0.0,
+ help="Minimum audio duration in seconds (e.g. 2.0)",
+ )
+ parser.add_argument(
+ "--max_length",
+ type=float,
+ default=float("inf"),
+ help="Maximum audio duration in seconds (e.g. 15.0)",
+ )
+ parser.add_argument(
+ "--num_machines",
+ type=int,
+ default=1,
+ help="Total number of machines for distributed runs",
+ )
+ parser.add_argument(
+ "--machine_index",
+ type=int,
+ default=0,
+ help="Zero-based machine index when distributing across multiple "
+ "machines (e.g. 0, 1, ... num_machines-1)",
+ )
+ parser.add_argument(
+ "--nj_per_gpu",
+ type=int,
+ default=3,
+ help="Number of worker processes to spawn per GPU.",
+ )
+ parser.add_argument(
+ "--loader_workers",
+ type=int,
+ default=24,
+ help="Number of DataLoader workers for streaming IterableDataset.",
+ )
+ parser.add_argument(
+ "--shuffle",
+ type=str2bool,
+ default=True,
+ help="Shuffle data by default.",
+ )
+ parser.add_argument(
+ "--shuffle-seed",
+ type=int,
+ default=42,
+ help="Random seed for shuffle (default: 42).",
+ )
+ return parser
+
+
+def count_lines(path):
+ with open(path, "rb") as f:
+ return sum(buf.count(b"\n") for buf in iter(lambda: f.read(1 << 20), b""))
+
+
+def serialise_numpy(key: str, tokens: np.ndarray) -> dict:
+ buffer = io.BytesIO()
+ np.save(buffer, tokens)
+ return {"__key__": key, "npy": buffer.getvalue()}
+
+
+def process_init(rank_queue, tokenizer_path):
+ """
+ Initialization function for each worker process.
+ Assigns a specific GPU to the process and loads the tokenizer.
+ """
+ global worker_tokenizer, worker_feature_extractor
+
+ # Configure worker process logging
+ formatter = (
+ "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d]"
+ " [Worker %(process)d] %(message)s"
+ )
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ # Get assigned GPU rank
+ rank = rank_queue.get()
+ # Determine device
+ if rank != -1 and torch.cuda.is_available():
+ worker_device = torch.device(f"cuda:{rank}")
+ else:
+ worker_device = torch.device("cpu")
+
+ logging.debug(f"Worker process initialized with device: {worker_device}")
+ # Load tokenizer onto the specified device
+ worker_feature_extractor = AutoFeatureExtractor.from_pretrained(tokenizer_path)
+ worker_tokenizer = HiggsAudioV2TokenizerModel.from_pretrained(
+ tokenizer_path, device_map=worker_device
+ )
+ logging.debug(f"Tokenizer loaded successfully on device {worker_device}")
+
+
+def process_single_sample(sample: dict[str, Any]) -> dict[str, Any]:
+ """
+ Single-sample processing function executed in worker processes.
+ Skips invalid samples during streaming processing.
+ """
+ try:
+ audio_tensor = sample.get("audio", None) # shape (1, T)
+ if audio_tensor is None:
+ raise ValueError("Sample missing 'audio' field")
+
+ with torch.inference_mode():
+ key = sample["label"]["id"]
+ inputs = worker_feature_extractor(
+ raw_audio=audio_tensor.squeeze(0).numpy(),
+ sampling_rate=HIGGS_INPUT_SAMPLE_RATE,
+ return_tensors="pt",
+ ).to(worker_tokenizer.device)
+ audio_tokens = worker_tokenizer.encode(
+ inputs["input_values"],
+ ).audio_codes.squeeze(0)
+
+ assert len(audio_tokens.shape) == 2
+ assert audio_tokens.size(0) == 8
+
+ num_tokens = audio_tokens.size(1)
+ metadata = sample["label"]
+ metadata["num_tokens"] = num_tokens
+
+ # Convert to numpy format for subsequent serialization (int16 to save space)
+ audio_tokens_np = audio_tokens.to(torch.int16).cpu().numpy()
+
+ return {
+ "status": "success",
+ "key": key,
+ "audio_tokens": audio_tokens_np,
+ "metadata": metadata,
+ "error_msg": None,
+ }
+ except Exception as e:
+ sample_id = sample.get("label", {}).get("id", "unknown")
+ logging.error(f"Failed to process sample {sample_id}: {e}")
+ return {
+ "status": "error",
+ "key": sample_id,
+ "audio_tokens": None,
+ "metadata": None,
+ "error_msg": str(e),
+ }
+
+
+def _normalise_value(value: Any) -> Any:
+ """Convert tensors and NumPy scalars to serialisable Python objects."""
+ if isinstance(value, torch.Tensor):
+ if value.ndim == 0:
+ return value.item()
+ return value.cpu().tolist()
+ if isinstance(value, np.generic):
+ return value.item()
+ if isinstance(value, np.ndarray):
+ return value.tolist()
+ return value
+
+
+def _encode_metadata(metadata: dict[str, Any]) -> bytes:
+ cleaned: dict[str, Any] = {}
+ for key, value in metadata.items():
+ if value is None:
+ continue
+ cleaned[key] = _normalise_value(value)
+ return json.dumps(cleaned, ensure_ascii=False).encode("utf-8")
+
+
+class StreamingLengthFilteredDataset(IterableDataset):
+ def __init__(
+ self,
+ base_iterable,
+ min_len: float,
+ max_len: float,
+ sr: int,
+ ):
+ self.base_iterable = base_iterable
+ self.min_len = min_len
+ self.max_len = max_len
+ self.sr = sr
+ self.filtered_count = 0
+
+ def __iter__(self):
+ """Stream samples one by one and filter on the fly."""
+ for sample in self.base_iterable:
+ try:
+ duration = sample["audio"].size(-1) / self.sr
+ if self.min_len <= duration <= self.max_len:
+ yield sample
+ else:
+ self.filtered_count += 1
+ logging.warning(
+ f"Filtered sample (duration out of range): "
+ f"{sample['label']['id']} ({duration:.2f}s)"
+ )
+ except Exception as e:
+ logging.warning(f"Skipped invalid sample during streaming: {e}")
+ continue
+
+
+def main() -> None:
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+ parser = build_parser()
+ args = parser.parse_args()
+ mp.set_start_method("spawn", force=True)
+
+ # Validate input arguments
+ assert bool(args.input_manifest) != bool(
+ args.input_jsonl
+ ), "Exactly one of --input_manifest or --input_jsonl must be provided."
+
+ if args.num_machines > 1:
+ assert (
+ 0 <= args.machine_index < args.num_machines
+ ), f"machine_index {args.machine_index} must be in [0, {args.num_machines})"
+
+ # Build base dataset and count total samples based on input mode
+ if args.input_jsonl:
+ logging.info(f"Input mode: raw JSONL ({args.input_jsonl})")
+ total_samples = count_lines(args.input_jsonl)
+ base_dataset = JsonlDatasetReader(
+ args.input_jsonl,
+ sample_rate=HIGGS_INPUT_SAMPLE_RATE,
+ shuffle=args.shuffle,
+ shuffle_seed=args.shuffle_seed,
+ )
+ loader_workers = args.loader_workers
+ else:
+ logging.info(f"Input mode: WebDataset manifest ({args.input_manifest})")
+ manifest_num_lines = count_lines(args.input_manifest)
+ loader_workers = min(args.loader_workers, manifest_num_lines)
+ total_samples = 0
+ manifests = []
+ with open(args.input_manifest, "r", encoding="utf-8") as f:
+ for line_id, line in tqdm(
+ enumerate(f),
+ total=manifest_num_lines,
+ desc="Calculating dataset length",
+ ):
+ items = line.strip().split(" ")
+ tar_path, jsonl_path, num_items, duration = (
+ items[0],
+ items[1],
+ int(items[2]),
+ float(items[3]),
+ )
+ assert os.path.exists(tar_path), f"File {tar_path} does not exist."
+ assert os.path.exists(jsonl_path), f"File {jsonl_path} does not exist."
+ assert jsonl_path.endswith(
+ ".jsonl"
+ ), f"File {jsonl_path} is not a .jsonl file."
+ if (
+ args.num_machines > 1
+ and line_id % args.num_machines != args.machine_index
+ ):
+ continue
+ total_samples += num_items
+ manifests.append((tar_path, jsonl_path, num_items, duration))
+ logging.info(
+ f"Total shards: {manifest_num_lines}, "
+ f"Shards for current index: {len(manifests)}"
+ )
+ base_dataset = WebDatasetReader(
+ manifests=manifests,
+ sample_rate=HIGGS_INPUT_SAMPLE_RATE,
+ evaluation=True,
+ )
+
+ # Adjust samples_per_shard if min_num_shards would be violated
+ samples_per_shard = args.samples_per_shard
+ if total_samples > 0:
+ estimated_shards = max(
+ 1, (total_samples + samples_per_shard - 1) // samples_per_shard
+ )
+ if estimated_shards < args.min_num_shards:
+ samples_per_shard = max(1, total_samples // args.min_num_shards)
+ logging.info(
+ f"Adjusted samples_per_shard from {args.samples_per_shard} to "
+ f"{samples_per_shard} to meet min_num_shards={args.min_num_shards} "
+ f"(total_samples={total_samples})"
+ )
+
+ # Apply length filter and create DataLoader
+ filtered_dataset = StreamingLengthFilteredDataset(
+ base_iterable=base_dataset,
+ min_len=args.min_length,
+ max_len=args.max_length,
+ sr=HIGGS_INPUT_SAMPLE_RATE,
+ )
+ dataloader = DataLoader(
+ dataset=filtered_dataset,
+ batch_size=None,
+ num_workers=loader_workers,
+ persistent_workers=loader_workers > 0,
+ pin_memory=False,
+ )
+
+ # Configure multi-GPU multi-process setup
+ num_devices = torch.cuda.device_count()
+ if num_devices == 0:
+ logging.warning("No GPUs detected - using CPU for processing")
+ num_processes = args.nj_per_gpu
+ else:
+ num_processes = num_devices * args.nj_per_gpu
+ logging.info(
+ f"GPU count: {num_devices}, Processes per GPU: {args.nj_per_gpu}, "
+ f"Total processes: {num_processes}"
+ )
+
+ # Shared GPU rank queue for process assignment
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+ for rank in list(range(num_devices)) * args.nj_per_gpu:
+ rank_queue.put(rank)
+ if num_devices == 0:
+ for _ in range(num_processes):
+ rank_queue.put(-1)
+
+ # Prepare output paths
+ tar_output_pattern = str(Path(args.tar_output_pattern).expanduser())
+ jsonl_output_pattern = str(Path(args.jsonl_output_pattern).expanduser())
+ Path(tar_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+ Path(jsonl_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+
+ # Determine output directory from tar_output_pattern
+ output_dir = Path(tar_output_pattern).parent.parent
+ error_log_path = str(output_dir / "errors.jsonl")
+ manifest_path = str(output_dir / "data.lst")
+
+ # Setup error logger (writes to errors.jsonl)
+ error_logger = logging.getLogger("error_log")
+ error_logger.setLevel(logging.ERROR)
+ error_logger.handlers.clear()
+ error_fh = logging.FileHandler(error_log_path, mode="w", encoding="utf-8")
+ error_fh.setFormatter(logging.Formatter("%(message)s"))
+ error_logger.addHandler(error_fh)
+
+ # Progress and error tracking
+ processed_count = 0
+ error_count = 0
+ write_error_count = 0
+ failed_ids = []
+ shard_idx = 0
+ shard_sample_count = 0
+ shard_duration = 0.0
+ shard_manifest = {} # shard_idx -> (tar_path, jsonl_path, count, duration)
+
+ tar_writer = None
+ jsonl_file = None
+
+ def open_new_shard():
+ nonlocal tar_writer, jsonl_file, shard_idx, shard_sample_count, shard_duration
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ # Record manifest for the previous shard
+ if shard_idx > 0 and shard_sample_count > 0:
+ prev_idx = shard_idx - 1
+ shard_manifest[prev_idx] = (
+ os.path.abspath(tar_output_pattern % prev_idx),
+ os.path.abspath(jsonl_output_pattern % prev_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+ tar_fname = tar_output_pattern % shard_idx
+ jsonl_fname = jsonl_output_pattern % shard_idx
+ tar_writer = wds.TarWriter(tar_fname)
+ jsonl_file = open(jsonl_fname, "w", encoding="utf-8")
+ shard_idx += 1
+ shard_sample_count = 0
+ shard_duration = 0.0
+
+ def write_sample(key, audio_tokens_np, metadata):
+ nonlocal shard_sample_count, write_error_count, shard_duration
+ assert tar_writer is not None and jsonl_file is not None
+ try:
+ token_record = serialise_numpy(key, audio_tokens_np)
+ json_record = _encode_metadata(metadata)
+ tar_writer.write(token_record)
+ jsonl_file.write(json_record.decode("utf-8") + "\n")
+ shard_sample_count += 1
+ shard_duration += metadata.get("audio_duration", 0.0)
+ except Exception as exc:
+ write_error_count += 1
+ failed_ids.append(key)
+ error_logger.error(
+ json.dumps({"id": key, "reason": str(exc)}, ensure_ascii=False)
+ )
+ logging.error(f"Write failed for sample {key}: {exc}")
+
+ def handle_result(result):
+ nonlocal processed_count, error_count
+ if result["status"] == "success":
+ # Rotate shard if needed
+ if tar_writer is None or shard_sample_count >= samples_per_shard:
+ open_new_shard()
+ write_sample(result["key"], result["audio_tokens"], result["metadata"])
+ processed_count += 1
+ else:
+ error_count += 1
+ failed_ids.append(result["key"])
+ error_logger.error(
+ json.dumps(
+ {"id": result["key"], "reason": result["error_msg"]},
+ ensure_ascii=False,
+ )
+ )
+ if not args.skip_errors:
+ raise RuntimeError(
+ f"Sample {result['key']} processing failed due "
+ f"to {result['error_msg']} - terminating"
+ )
+ logging.warning(
+ f"Skipping failed sample {result['key']}: {result['error_msg']}"
+ )
+
+ main_progress = tqdm(total=total_samples, desc="Extracting Audio Tokens")
+
+ try:
+ with ProcessPoolExecutor(
+ max_workers=num_processes,
+ initializer=process_init,
+ initargs=(rank_queue, args.tokenizer_path),
+ ) as executor:
+ logging.info(f"Submitting tasks... ({num_processes} workers)")
+ futures = set()
+ max_pending = num_processes * 10
+
+ def drain_completed():
+ """Wait for at least one future to complete, process all done."""
+ nonlocal futures
+ done, _ = wait(futures, return_when=FIRST_COMPLETED)
+ for f in done:
+ futures.discard(f)
+ result = f.result()
+ main_progress.update(1)
+ handle_result(result)
+ main_progress.set_postfix(
+ Samples=processed_count,
+ Errors=error_count,
+ )
+
+ # Stream samples from DataLoader
+ for sample in dataloader:
+ if len(futures) >= max_pending:
+ drain_completed()
+
+ future = executor.submit(process_single_sample, sample)
+ futures.add(future)
+
+ # Process remaining futures
+ logging.info("Processing remaining pending samples...")
+ while futures:
+ drain_completed()
+
+ except Exception:
+ logging.error("Critical error during processing", exc_info=True)
+ raise
+ finally:
+ main_progress.close()
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ # Record the last shard in the manifest
+ if shard_idx > 0 and shard_sample_count > 0:
+ last_idx = shard_idx - 1
+ shard_manifest[last_idx] = (
+ os.path.abspath(tar_output_pattern % last_idx),
+ os.path.abspath(jsonl_output_pattern % last_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+
+ # Write manifest file (data.lst)
+ with open(manifest_path, "w", encoding="utf-8") as mf:
+ for idx in sorted(shard_manifest.keys()):
+ tar_path, jsonl_path, count, duration = shard_manifest[idx]
+ mf.write(f"{tar_path} {jsonl_path} {count} {duration:.3f}\n")
+
+ # Output final statistics
+ total_failed = error_count + write_error_count
+ filtered_and_skipped = total_samples - processed_count - total_failed
+ logging.info(
+ f"Processing Complete - Successful: {processed_count}, Failed: {total_failed}, "
+ f"Filtered/Skipped: {filtered_and_skipped}, Shards written: {shard_idx}"
+ )
+ logging.info(f"Manifest written to: {manifest_path} ({len(shard_manifest)} shards)")
+ if total_failed > 0:
+ logging.info(f"Error details: {error_log_path}")
+ if failed_ids and args.skip_errors:
+ logging.warning(
+ f"Failed sample IDs (count: {len(failed_ids)}): {failed_ids[:100]}..."
+ )
+ if write_error_count > 0 and not args.skip_errors:
+ raise RuntimeError(
+ f"{write_error_count} samples failed to write - check logs for details"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/scripts/extract_audio_tokens_add_noise.py b/omnivoice/scripts/extract_audio_tokens_add_noise.py
new file mode 100644
index 00000000..63aff438
--- /dev/null
+++ b/omnivoice/scripts/extract_audio_tokens_add_noise.py
@@ -0,0 +1,825 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Extract audio tokens from audio data and pack them into WebDataset shards.
+
+Extends ``extract_audio_tokens.py`` with optional noise and reverberation
+augmentation on the prompt (reference) portion of the audio. Requires a
+noise manifest and/or RIR manifest.
+
+Supports two input modes:
+
+1. WebDataset manifest (data.lst):
+ python extract_audio_tokens_add_noise.py \\
+ --input_manifest data.lst \\
+ --noise_manifest noise.lst \\
+ --tar_output_pattern output/audios/shard-%06d.tar \\
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl
+
+2. Raw JSONL (each line: {"id": "...", "audio_path": "...", "text": "...", ...}):
+ python extract_audio_tokens_add_noise.py \\
+ --input_jsonl data.jsonl \\
+ --noise_manifest noise.lst \\
+ --tar_output_pattern output/audios/shard-%06d.tar \\
+ --jsonl_output_pattern output/txts/shard-%06d.jsonl
+
+Output structure:
+ output_dir/
+ ├── audios/ # WebDataset tar shards (.npy audio tokens + .json metadata)
+ │ ├── shard_000000.tar
+ │ └── ...
+ ├── txts/ # Per-shard JSONL metadata
+ │ ├── shard_000000.jsonl
+ │ └── ...
+ ├── data.lst # Manifest:
+ └── errors.jsonl # Failed samples with error details
+"""
+
+import argparse
+import io
+import json
+import logging
+import math
+import multiprocessing as mp
+import os
+import random
+import warnings
+from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait
+from pathlib import Path
+from typing import Any
+
+import numpy as np
+import torch
+import torch.nn.functional as F
+import torchaudio
+import webdataset as wds
+from torch.utils.data import DataLoader, IterableDataset
+from tqdm.auto import tqdm
+from transformers import AutoFeatureExtractor, HiggsAudioV2TokenizerModel
+
+from omnivoice.data.dataset import JsonlDatasetReader, WebDatasetReader
+from omnivoice.utils.common import str2bool
+
+warnings.filterwarnings(
+ "ignore", category=FutureWarning, module="torch.nn.utils.weight_norm"
+)
+
+HIGGS_INPUT_SAMPLE_RATE = 24_000
+
+# Global variables: Store tokenizer and device for each worker process
+worker_tokenizer = None
+worker_feature_extractor = None
+worker_noise_sampler = None
+worker_rir_sampler = None
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--input_manifest",
+ default=None,
+ help="Path to input dataset manifest (data.lst).",
+ )
+ parser.add_argument(
+ "--input_jsonl",
+ default=None,
+ help="Path to raw JSONL file (alternative to --input_manifest).",
+ )
+ parser.add_argument(
+ "--tar_output_pattern",
+ required=True,
+ help="Tar shard pattern passed to WebDataset",
+ )
+ parser.add_argument(
+ "--jsonl_output_pattern",
+ required=True,
+ help="Jsonl shard pattern passed to WebDataset",
+ )
+ parser.add_argument(
+ "--samples_per_shard",
+ type=int,
+ default=1000,
+ help="Maximum records per shard",
+ )
+ parser.add_argument(
+ "--min_num_shards",
+ type=int,
+ default=32,
+ help="Minimum number of output shards (use to ensure "
+ "shard count >= num_gpu * num_workers)",
+ )
+ parser.add_argument(
+ "--tokenizer_path",
+ type=str,
+ default="eustlb/higgs-audio-v2-tokenizer",
+ help="Path to audio tokenizer.",
+ )
+ parser.add_argument(
+ "--skip_errors", action="store_true", help="Skip items that fail to process"
+ )
+ parser.add_argument(
+ "--min_length",
+ type=float,
+ default=0.0,
+ help="Minimum audio duration in seconds (e.g. 2.0)",
+ )
+ parser.add_argument(
+ "--max_length",
+ type=float,
+ default=float("inf"),
+ help="Maximum audio duration in seconds (e.g. 15.0)",
+ )
+ parser.add_argument(
+ "--num_machines",
+ type=int,
+ default=1,
+ help="Total number of machines for distributed runs",
+ )
+ parser.add_argument(
+ "--machine_index",
+ type=int,
+ default=0,
+ help="Zero-based machine index when distributing across multiple "
+ "machines (e.g. 0, 1, ... num_machines-1)",
+ )
+ parser.add_argument(
+ "--nj_per_gpu",
+ type=int,
+ default=3,
+ help="Number of worker processes to spawn per GPU.",
+ )
+ parser.add_argument(
+ "--loader_workers",
+ type=int,
+ default=24,
+ help="Number of DataLoader workers for streaming IterableDataset.",
+ )
+ parser.add_argument(
+ "--shuffle",
+ type=str2bool,
+ default=True,
+ help="Shuffle data by default.",
+ )
+ parser.add_argument(
+ "--shuffle-seed",
+ type=int,
+ default=42,
+ help="Random seed for shuffle (default: 42).",
+ )
+ parser.add_argument(
+ "--noise_manifest",
+ default=None,
+ help="Path to noise manifest (list of tar files). Enables prompt noise augmentation.",
+ )
+ parser.add_argument(
+ "--rir_manifest",
+ default=None,
+ help="Path to RIR manifest (list of tar files). Enables prompt reverb augmentation.",
+ )
+ return parser
+
+
+def count_lines(path):
+ with open(path, "rb") as f:
+ return sum(buf.count(b"\n") for buf in iter(lambda: f.read(1 << 20), b""))
+
+
+def serialise_numpy(key: str, tokens: np.ndarray) -> dict:
+ buffer = io.BytesIO()
+ np.save(buffer, tokens)
+ return {"__key__": key, "npy": buffer.getvalue()}
+
+
+def _load_aug_audio(data, sample_rate=24000):
+ """Simple audio loader for augmentation files."""
+ with io.BytesIO(data) as b:
+ wav, sr = torchaudio.load(b)
+ if wav.shape[0] > 1:
+ wav = wav.mean(dim=0, keepdim=True)
+ if sr != sample_rate:
+ wav = torchaudio.functional.resample(wav, sr, sample_rate)
+ return wav
+
+
+class SimpleWorkerSampler:
+ """A lightweight infinite sampler for noise/RIR within a worker process."""
+
+ def __init__(self, tar_paths, sample_rate=24000):
+ self.dataset = (
+ wds.WebDataset(
+ tar_paths, shardshuffle=True, nodesplitter=None, workersplitter=None
+ )
+ .decode()
+ .map(lambda s: self._decode(s, sample_rate))
+ .select(lambda x: x is not None)
+ .shuffle(100)
+ .repeat()
+ )
+ self.iterator = iter(self.dataset)
+
+ def _decode(self, sample, sample_rate):
+ for ext in ["wav", "flac", "mp3"]:
+ if ext in sample:
+ return _load_aug_audio(sample[ext], sample_rate)
+ return None
+
+ def sample_segment(self, target_len, allow_repeat=True):
+ """Get a random segment of noise matching the target length."""
+ try:
+ audio = next(self.iterator)
+ except StopIteration:
+ self.iterator = iter(self.dataset)
+ audio = next(self.iterator)
+
+ cur_len = audio.size(-1)
+ if cur_len < target_len and allow_repeat:
+ if cur_len > 0:
+ num_repeats = math.ceil(target_len / cur_len)
+ audio = audio.repeat(1, num_repeats)
+ else:
+ audio = F.pad(audio, (0, target_len), mode="constant")
+ cur_len = audio.size(-1)
+
+ if cur_len > target_len:
+ start = random.randint(0, cur_len - target_len)
+ audio = audio[..., start : start + target_len]
+
+ return audio
+
+
+def _convolve1d(signal: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor:
+ m = signal.size(-1)
+ n = kernel.size(-1)
+ padded_size = m + n - 1
+ f_signal = torch.fft.rfft(signal, n=padded_size)
+ f_kernel = torch.fft.rfft(kernel, n=padded_size)
+ f_result = f_signal * f_kernel
+ result = torch.fft.irfft(f_result, n=padded_size)
+ return result[:padded_size]
+
+
+def _apply_rir(audio, rir, mix_ratio=0.5):
+ rir_scaling_factor = 0.5**15
+ N_in = audio.shape[-1]
+ rir_d = rir[0, :] * rir_scaling_factor
+ aug_d = _convolve1d(audio[0], rir_d)
+ shift_index = torch.argmax(torch.abs(rir_d))
+ end_index = shift_index + N_in
+ if end_index > aug_d.shape[0]:
+ augmented = F.pad(aug_d[shift_index:], (0, end_index - aug_d.shape[0]))
+ else:
+ augmented = aug_d[shift_index:end_index]
+ power_before = torch.sum(audio[0] ** 2)
+ power_after = torch.sum(augmented**2)
+ if power_after > 0:
+ augmented *= torch.sqrt(power_before / power_after)
+ mixed = (1 - mix_ratio) * audio[0] + mix_ratio * augmented
+ return mixed.unsqueeze(0)
+
+
+def process_init(rank_queue, tokenizer_path, noise_manifest=None, rir_manifest=None):
+ """
+ Initialization function for each worker process.
+ Assigns a specific GPU to the process and loads the tokenizer.
+ """
+ global worker_tokenizer, worker_feature_extractor, worker_noise_sampler, worker_rir_sampler
+
+ # Configure worker process logging
+ formatter = (
+ "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d]"
+ " [Worker %(process)d] %(message)s"
+ )
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+
+ # Get assigned GPU rank
+ rank = rank_queue.get()
+ # Determine device
+ if rank != -1 and torch.cuda.is_available():
+ worker_device = torch.device(f"cuda:{rank}")
+ else:
+ worker_device = torch.device("cpu")
+
+ logging.debug(f"Worker process initialized with device: {worker_device}")
+ # Load tokenizer onto the specified device
+ worker_feature_extractor = AutoFeatureExtractor.from_pretrained(tokenizer_path)
+ worker_tokenizer = HiggsAudioV2TokenizerModel.from_pretrained(
+ tokenizer_path, device_map=worker_device
+ )
+ logging.debug(f"Tokenizer loaded successfully on device {worker_device}")
+
+ # Initialize augmentation samplers (optional)
+ if noise_manifest:
+ try:
+ with open(noise_manifest, "r") as f:
+ tars = [l.strip().split()[0] for l in f if l.strip()]
+ worker_noise_sampler = SimpleWorkerSampler(
+ tars, sample_rate=HIGGS_INPUT_SAMPLE_RATE
+ )
+ logging.debug("Noise sampler initialized.")
+ except Exception as e:
+ logging.warning(f"Failed to load noise manifest: {e}")
+
+ if rir_manifest:
+ try:
+ with open(rir_manifest, "r") as f:
+ tars = [l.strip().split()[0] for l in f if l.strip()]
+ worker_rir_sampler = SimpleWorkerSampler(
+ tars, sample_rate=HIGGS_INPUT_SAMPLE_RATE
+ )
+ logging.debug("RIR sampler initialized.")
+ except Exception as e:
+ logging.warning(f"Failed to load RIR manifest: {e}")
+
+
+def _augment_prompt(audio_tensor: torch.Tensor) -> tuple[torch.Tensor, int]:
+ """Apply noise/reverb augmentation to the front portion of audio.
+
+ Returns the augmented audio and the sample index where clean audio starts.
+ """
+ # Pre-normalization
+ max_val = audio_tensor.abs().max() + 1e-7
+ audio_tensor = (audio_tensor / max_val) * 0.6
+
+ total_len = audio_tensor.size(-1)
+ ratio = random.uniform(0.1, 0.3)
+ split_idx = int(total_len * ratio)
+ front_part = audio_tensor[:, :split_idx].clone()
+
+ # Apply noise
+ if worker_noise_sampler is not None:
+ noise = worker_noise_sampler.sample_segment(split_idx)
+ snr_db = random.uniform(5, 15)
+ sig_rms = front_part.norm(p=2) / (split_idx**0.5)
+ noise_rms = noise.norm(p=2) / (split_idx**0.5)
+ if noise_rms > 1e-9:
+ snr = 10 ** (snr_db / 20)
+ scale = sig_rms / (snr * noise_rms + 1e-8)
+ front_part = front_part + noise * scale
+
+ # Apply RIR (30% probability)
+ if worker_rir_sampler is not None and random.random() < 0.3:
+ rir = worker_rir_sampler.sample_segment(split_idx, allow_repeat=False)
+ reverb_amt = random.uniform(0.3, 1.0)
+ try:
+ front_part = _apply_rir(front_part, rir, reverb_amt)
+ except Exception as e:
+ logging.warning(f"RIR failed: {e}")
+
+ # Merge back
+ if front_part.device != audio_tensor.device:
+ front_part = front_part.to(audio_tensor.device)
+ audio_tensor[:, :split_idx] = front_part
+
+ # Post-normalization
+ max_val = audio_tensor.abs().max() + 1e-7
+ audio_tensor = (audio_tensor / max_val) * 0.9
+
+ return audio_tensor, split_idx
+
+
+def process_single_sample(sample: dict[str, Any]) -> dict[str, Any]:
+ """
+ Single-sample processing function executed in worker processes.
+ Skips invalid samples during streaming processing.
+ """
+ try:
+ audio_tensor = sample.get("audio", None) # shape (1, T)
+ if audio_tensor is None:
+ raise ValueError("Sample missing 'audio' field")
+
+ # Apply prompt augmentation if noise/rir samplers are available
+ enable_aug = worker_noise_sampler is not None or worker_rir_sampler is not None
+ clean_sample_idx = 0
+ if enable_aug:
+ audio_tensor, clean_sample_idx = _augment_prompt(audio_tensor)
+
+ with torch.inference_mode():
+ key = sample["label"]["id"]
+
+ inputs = worker_feature_extractor(
+ raw_audio=audio_tensor.squeeze(0).numpy(),
+ sampling_rate=HIGGS_INPUT_SAMPLE_RATE,
+ return_tensors="pt",
+ ).to(worker_tokenizer.device)
+ audio_tokens = worker_tokenizer.encode(
+ inputs["input_values"],
+ ).audio_codes.squeeze(0)
+
+ assert len(audio_tokens.shape) == 2
+ assert audio_tokens.size(0) == 8
+
+ num_tokens = audio_tokens.size(1)
+ metadata = sample["label"]
+ metadata["num_tokens"] = num_tokens
+
+ if enable_aug:
+ clean_token_idx = math.ceil(
+ clean_sample_idx / worker_tokenizer.config.hop_length
+ )
+ metadata["clean_start_token_idx"] = clean_token_idx
+
+ # Convert to numpy format for subsequent serialization (int16 to save space)
+ audio_tokens_np = audio_tokens.to(torch.int16).cpu().numpy()
+
+ return {
+ "status": "success",
+ "key": key,
+ "audio_tokens": audio_tokens_np,
+ "metadata": metadata,
+ "error_msg": None,
+ }
+ except Exception as e:
+ sample_id = sample.get("label", {}).get("id", "unknown")
+ logging.error(f"Failed to process sample {sample_id}: {e}")
+ return {
+ "status": "error",
+ "key": sample_id,
+ "audio_tokens": None,
+ "metadata": None,
+ "error_msg": str(e),
+ }
+
+
+def _normalise_value(value: Any) -> Any:
+ """Convert tensors and NumPy scalars to serialisable Python objects."""
+ if isinstance(value, torch.Tensor):
+ if value.ndim == 0:
+ return value.item()
+ return value.cpu().tolist()
+ if isinstance(value, np.generic):
+ return value.item()
+ if isinstance(value, np.ndarray):
+ return value.tolist()
+ return value
+
+
+def _encode_metadata(metadata: dict[str, Any]) -> bytes:
+ cleaned: dict[str, Any] = {}
+ for key, value in metadata.items():
+ if value is None:
+ continue
+ cleaned[key] = _normalise_value(value)
+ return json.dumps(cleaned, ensure_ascii=False).encode("utf-8")
+
+
+class StreamingLengthFilteredDataset(IterableDataset):
+ def __init__(
+ self,
+ base_iterable,
+ min_len: float,
+ max_len: float,
+ sr: int,
+ ):
+ self.base_iterable = base_iterable
+ self.min_len = min_len
+ self.max_len = max_len
+ self.sr = sr
+ self.filtered_count = 0
+
+ def __iter__(self):
+ """Stream samples one by one and filter on the fly."""
+ for sample in self.base_iterable:
+ try:
+ duration = sample["audio"].size(-1) / self.sr
+ if self.min_len <= duration <= self.max_len:
+ yield sample
+ else:
+ self.filtered_count += 1
+ logging.warning(
+ f"Filtered sample (duration out of range): "
+ f"{sample['label']['id']} ({duration:.2f}s)"
+ )
+ except Exception as e:
+ logging.warning(f"Skipped invalid sample during streaming: {e}")
+ continue
+
+
+def main() -> None:
+ formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
+ logging.basicConfig(format=formatter, level=logging.INFO, force=True)
+ parser = build_parser()
+ args = parser.parse_args()
+ mp.set_start_method("spawn", force=True)
+
+ # Validate input arguments
+ assert bool(args.input_manifest) != bool(
+ args.input_jsonl
+ ), "Exactly one of --input_manifest or --input_jsonl must be provided."
+
+ if args.num_machines > 1:
+ assert (
+ 0 <= args.machine_index < args.num_machines
+ ), f"machine_index {args.machine_index} must be in [0, {args.num_machines})"
+
+ # Build base dataset and count total samples based on input mode
+ if args.input_jsonl:
+ logging.info(f"Input mode: raw JSONL ({args.input_jsonl})")
+ total_samples = count_lines(args.input_jsonl)
+ base_dataset = JsonlDatasetReader(
+ args.input_jsonl,
+ sample_rate=HIGGS_INPUT_SAMPLE_RATE,
+ shuffle=args.shuffle,
+ shuffle_seed=args.shuffle_seed,
+ )
+ loader_workers = args.loader_workers
+ else:
+ logging.info(f"Input mode: WebDataset manifest ({args.input_manifest})")
+ manifest_num_lines = count_lines(args.input_manifest)
+ loader_workers = min(args.loader_workers, manifest_num_lines)
+ total_samples = 0
+ manifests = []
+ with open(args.input_manifest, "r", encoding="utf-8") as f:
+ for line_id, line in tqdm(
+ enumerate(f),
+ total=manifest_num_lines,
+ desc="Calculating dataset length",
+ ):
+ items = line.strip().split(" ")
+ tar_path, jsonl_path, num_items, duration = (
+ items[0],
+ items[1],
+ int(items[2]),
+ float(items[3]),
+ )
+ assert os.path.exists(tar_path), f"File {tar_path} does not exist."
+ assert os.path.exists(jsonl_path), f"File {jsonl_path} does not exist."
+ assert jsonl_path.endswith(
+ ".jsonl"
+ ), f"File {jsonl_path} is not a .jsonl file."
+ if (
+ args.num_machines > 1
+ and line_id % args.num_machines != args.machine_index
+ ):
+ continue
+ total_samples += num_items
+ manifests.append((tar_path, jsonl_path, num_items, duration))
+ logging.info(
+ f"Total shards: {manifest_num_lines}, "
+ f"Shards for current index: {len(manifests)}"
+ )
+ base_dataset = WebDatasetReader(
+ manifests=manifests,
+ sample_rate=HIGGS_INPUT_SAMPLE_RATE,
+ evaluation=True,
+ )
+
+ # Apply length filter and create DataLoader
+ filtered_dataset = StreamingLengthFilteredDataset(
+ base_iterable=base_dataset,
+ min_len=args.min_length,
+ max_len=args.max_length,
+ sr=HIGGS_INPUT_SAMPLE_RATE,
+ )
+ dataloader = DataLoader(
+ dataset=filtered_dataset,
+ batch_size=None,
+ num_workers=loader_workers,
+ persistent_workers=loader_workers > 0,
+ pin_memory=False,
+ )
+
+ # Adjust samples_per_shard if min_num_shards would be violated
+ samples_per_shard = args.samples_per_shard
+ if total_samples > 0:
+ estimated_shards = max(
+ 1, (total_samples + samples_per_shard - 1) // samples_per_shard
+ )
+ if estimated_shards < args.min_num_shards:
+ samples_per_shard = max(1, total_samples // args.min_num_shards)
+ logging.info(
+ f"Adjusted samples_per_shard from {args.samples_per_shard} to "
+ f"{samples_per_shard} to meet min_num_shards={args.min_num_shards} "
+ f"(total_samples={total_samples})"
+ )
+
+ # Configure multi-GPU multi-process setup
+ num_devices = torch.cuda.device_count()
+ if num_devices == 0:
+ logging.warning("No GPUs detected - using CPU for processing")
+ num_processes = args.nj_per_gpu
+ else:
+ num_processes = num_devices * args.nj_per_gpu
+ logging.info(
+ f"GPU count: {num_devices}, Processes per GPU: {args.nj_per_gpu}, "
+ f"Total processes: {num_processes}"
+ )
+ if args.noise_manifest or args.rir_manifest:
+ logging.info(
+ f"Prompt augmentation enabled - "
+ f"noise: {args.noise_manifest or 'off'}, rir: {args.rir_manifest or 'off'}"
+ )
+
+ # Shared GPU rank queue for process assignment
+ manager = mp.Manager()
+ rank_queue = manager.Queue()
+ for rank in list(range(num_devices)) * args.nj_per_gpu:
+ rank_queue.put(rank)
+ if num_devices == 0:
+ for _ in range(num_processes):
+ rank_queue.put(-1)
+
+ # Prepare output paths
+ tar_output_pattern = str(Path(args.tar_output_pattern).expanduser())
+ jsonl_output_pattern = str(Path(args.jsonl_output_pattern).expanduser())
+ Path(tar_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+ Path(jsonl_output_pattern).parent.mkdir(parents=True, exist_ok=True)
+
+ # Determine output directory from tar_output_pattern
+ output_dir = Path(tar_output_pattern).parent.parent
+ error_log_path = str(output_dir / "errors.jsonl")
+ manifest_path = str(output_dir / "data.lst")
+
+ # Setup error logger (writes to errors.jsonl)
+ error_logger = logging.getLogger("error_log")
+ error_logger.setLevel(logging.ERROR)
+ error_logger.handlers.clear()
+ error_fh = logging.FileHandler(error_log_path, mode="w", encoding="utf-8")
+ error_fh.setFormatter(logging.Formatter("%(message)s"))
+ error_logger.addHandler(error_fh)
+
+ # Progress and error tracking
+ processed_count = 0
+ error_count = 0
+ write_error_count = 0
+ failed_ids = []
+ shard_idx = 0
+ shard_sample_count = 0
+ shard_duration = 0.0
+ shard_manifest = {} # shard_idx -> (tar_path, jsonl_path, count, duration)
+
+ tar_writer = None
+ jsonl_file = None
+
+ def open_new_shard():
+ nonlocal tar_writer, jsonl_file, shard_idx, shard_sample_count, shard_duration
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ # Record manifest for the previous shard
+ if shard_idx > 0 and shard_sample_count > 0:
+ prev_idx = shard_idx - 1
+ shard_manifest[prev_idx] = (
+ os.path.abspath(tar_output_pattern % prev_idx),
+ os.path.abspath(jsonl_output_pattern % prev_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+ tar_fname = tar_output_pattern % shard_idx
+ jsonl_fname = jsonl_output_pattern % shard_idx
+ tar_writer = wds.TarWriter(tar_fname)
+ jsonl_file = open(jsonl_fname, "w", encoding="utf-8")
+ shard_idx += 1
+ shard_sample_count = 0
+ shard_duration = 0.0
+
+ def write_sample(key, audio_tokens_np, metadata):
+ nonlocal shard_sample_count, write_error_count, shard_duration
+ assert tar_writer is not None and jsonl_file is not None
+ try:
+ token_record = serialise_numpy(key, audio_tokens_np)
+ json_record = _encode_metadata(metadata)
+ tar_writer.write(token_record)
+ jsonl_file.write(json_record.decode("utf-8") + "\n")
+ shard_sample_count += 1
+ shard_duration += metadata.get("audio_duration", 0.0)
+ except Exception as exc:
+ write_error_count += 1
+ failed_ids.append(key)
+ error_logger.error(
+ json.dumps({"id": key, "reason": str(exc)}, ensure_ascii=False)
+ )
+ logging.error(f"Write failed for sample {key}: {exc}")
+
+ def handle_result(result):
+ nonlocal processed_count, error_count
+ if result["status"] == "success":
+ # Rotate shard if needed
+ if tar_writer is None or shard_sample_count >= samples_per_shard:
+ open_new_shard()
+ write_sample(result["key"], result["audio_tokens"], result["metadata"])
+ processed_count += 1
+ else:
+ error_count += 1
+ failed_ids.append(result["key"])
+ error_logger.error(
+ json.dumps(
+ {"id": result["key"], "reason": result["error_msg"]},
+ ensure_ascii=False,
+ )
+ )
+ if not args.skip_errors:
+ raise RuntimeError(
+ f"Sample {result['key']} processing failed due "
+ f"to {result['error_msg']} - terminating"
+ )
+ logging.warning(
+ f"Skipping failed sample {result['key']}: {result['error_msg']}"
+ )
+
+ main_progress = tqdm(total=total_samples, desc="Extracting Audio Tokens")
+
+ try:
+ with ProcessPoolExecutor(
+ max_workers=num_processes,
+ initializer=process_init,
+ initargs=(
+ rank_queue,
+ args.tokenizer_path,
+ args.noise_manifest,
+ args.rir_manifest,
+ ),
+ ) as executor:
+ logging.info(f"Submitting tasks... ({num_processes} workers)")
+ futures = set()
+ max_pending = num_processes * 10
+
+ def drain_completed():
+ """Wait for at least one future to complete, process all done."""
+ nonlocal futures
+ done, _ = wait(futures, return_when=FIRST_COMPLETED)
+ for f in done:
+ futures.discard(f)
+ result = f.result()
+ main_progress.update(1)
+ handle_result(result)
+ main_progress.set_postfix(
+ Samples=processed_count,
+ Errors=error_count,
+ )
+
+ # Stream samples from DataLoader
+ for sample in dataloader:
+ if len(futures) >= max_pending:
+ drain_completed()
+
+ future = executor.submit(process_single_sample, sample)
+ futures.add(future)
+
+ # Process remaining futures
+ logging.info("Processing remaining pending samples...")
+ while futures:
+ drain_completed()
+
+ except Exception:
+ logging.error("Critical error during processing", exc_info=True)
+ raise
+ finally:
+ main_progress.close()
+ if tar_writer is not None:
+ tar_writer.close()
+ if jsonl_file is not None:
+ jsonl_file.close()
+ # Record the last shard in the manifest
+ if shard_idx > 0 and shard_sample_count > 0:
+ last_idx = shard_idx - 1
+ shard_manifest[last_idx] = (
+ os.path.abspath(tar_output_pattern % last_idx),
+ os.path.abspath(jsonl_output_pattern % last_idx),
+ shard_sample_count,
+ shard_duration,
+ )
+
+ # Write manifest file (data.lst)
+ with open(manifest_path, "w", encoding="utf-8") as mf:
+ for idx in sorted(shard_manifest.keys()):
+ tar_path, jsonl_path, count, duration = shard_manifest[idx]
+ mf.write(f"{tar_path} {jsonl_path} {count} {duration:.3f}\n")
+
+ # Output final statistics
+ total_failed = error_count + write_error_count
+ filtered_and_skipped = total_samples - processed_count - total_failed
+ logging.info(
+ f"Processing Complete - Successful: {processed_count}, Failed: {total_failed}, "
+ f"Filtered/Skipped: {filtered_and_skipped}, Shards written: {shard_idx}"
+ )
+ logging.info(f"Manifest written to: {manifest_path} ({len(shard_manifest)} shards)")
+ if total_failed > 0:
+ logging.info(f"Error details: {error_log_path}")
+ if failed_ids and args.skip_errors:
+ logging.warning(
+ f"Failed sample IDs (count: {len(failed_ids)}): {failed_ids[:100]}..."
+ )
+ if write_error_count > 0 and not args.skip_errors:
+ raise RuntimeError(
+ f"{write_error_count} samples failed to write - check logs for details"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/omnivoice/scripts/jsonl_to_webdataset.py b/omnivoice/scripts/jsonl_to_webdataset.py
new file mode 100644
index 00000000..b3df95ac
--- /dev/null
+++ b/omnivoice/scripts/jsonl_to_webdataset.py
@@ -0,0 +1,439 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Pack a JSONL audio dataset into a customed WebDataset shards
+(paired .tar and .jsonl files).
+
+Usage:
+ python jsonl_to_webdataset.py \
+ --input data.jsonl \
+ --output output_dir/ \
+ --workers 16 \
+ --threads 4 \
+ --shard-size 1000 \
+ --sr 24000
+
+Input JSONL format (one JSON object per line):
+ {"id": "utt_001", "audio_path": "/data/wavs/001.wav", "text": "hello world", ...}
+
+ Required fields: "id", "audio_path", "text"
+ All other fields are preserved in the output metadata.
+
+Output structure:
+ output_dir/
+ ├── audios/ # WebDataset tar shards
+ │ ├── shard_000000.tar
+ │ ├── shard_000001.tar
+ │ └── ...
+ ├── txts/ # Per-shard JSONL metadata (with audio_duration added)
+ │ ├── shard_000000.jsonl
+ │ ├── shard_000001.jsonl
+ │ └── ...
+ ├── data.lst # Manifest:
+ └── errors.jsonl # Failed samples with error details
+"""
+
+import argparse
+import io
+import json
+import logging
+import multiprocessing as mp
+import os
+import random
+from concurrent.futures import (
+ FIRST_COMPLETED,
+ ProcessPoolExecutor,
+ ThreadPoolExecutor,
+ as_completed,
+ wait,
+)
+from itertools import islice
+from pathlib import Path
+
+import torchaudio
+import webdataset as wds
+from tqdm import tqdm
+
+from omnivoice.utils.common import str2bool
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ description="Pack JSONL audio dataset into WebDataset shards."
+ )
+ parser.add_argument(
+ "--input", type=str, default="data.jsonl", help="Path to input JSONL file"
+ )
+ parser.add_argument(
+ "--output",
+ type=str,
+ default="emilia",
+ help="Path to output directory",
+ )
+ parser.add_argument(
+ "--workers",
+ type=int,
+ default=16,
+ help="Number of worker processes (default: 16)",
+ )
+ parser.add_argument(
+ "--threads",
+ type=int,
+ default=4,
+ help="Number of threads per worker process.",
+ )
+ parser.add_argument(
+ "--shard-size",
+ type=int,
+ default=1000,
+ help="Number of samples per shard (default: 1000)",
+ )
+ parser.add_argument(
+ "--sr", type=int, default=24000, help="Target sample rate (default: 24000)"
+ )
+ parser.add_argument(
+ "--shuffle",
+ type=str2bool,
+ default=True,
+ help="Shuffle data by default.",
+ )
+ parser.add_argument(
+ "--shuffle-seed",
+ type=int,
+ default=42,
+ help="Random seed for shuffle (default: 42)",
+ )
+ parser.add_argument(
+ "--min-duration",
+ type=float,
+ default=None,
+ help="Filter out samples shorter than this (seconds).",
+ )
+ parser.add_argument(
+ "--max-duration",
+ type=float,
+ default=None,
+ help="Filter out samples >= this duration (seconds).",
+ )
+ return parser
+
+
+def read_jsonl(file_path):
+ with open(file_path, "r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ yield json.loads(line)
+
+
+def chunked_reader(iterator, chunk_size):
+ it = iter(iterator)
+ while chunk := list(islice(it, chunk_size)):
+ yield chunk
+
+
+def process_audio_item(meta, target_sr):
+ key = meta.get("id")
+ audio_path = meta.get("audio_path")
+
+ if not key or not audio_path:
+ return {
+ "error": {
+ "id": key,
+ "audio_path": audio_path,
+ "reason": "missing id or audio_path",
+ }
+ }
+
+ try:
+ if not os.path.exists(audio_path):
+ raise FileNotFoundError(f"{audio_path} not found")
+
+ waveform, sr = torchaudio.load(audio_path)
+ audio_duration = waveform.shape[1] / sr
+ meta["audio_duration"] = audio_duration
+
+ if target_sr and sr != target_sr:
+ waveform = torchaudio.functional.resample(waveform, sr, target_sr)
+ sr = target_sr
+
+ audio_buffer = io.BytesIO()
+ torchaudio.save(audio_buffer, waveform, sr, format="flac", bits_per_sample=16)
+ audio_bytes = audio_buffer.getvalue()
+
+ sample = {
+ "__key__": key,
+ "flac": audio_bytes,
+ }
+
+ return {"ok": (sample, meta)}
+
+ except Exception as e:
+ return {"error": {"id": key, "audio_path": audio_path, "reason": str(e)}}
+
+
+def process_single_shard(
+ shard_idx,
+ records,
+ output_tar_pattern,
+ output_jsonl_pattern,
+ target_sr,
+ num_threads=4,
+ min_duration=None,
+ max_duration=None,
+):
+ tar_fname = output_tar_pattern % shard_idx
+ jsonl_fname = output_jsonl_pattern % shard_idx
+
+ processed_count = 0
+ filtered_count = 0
+ error_count = 0
+ total_duration = 0.0
+ errors = []
+
+ with wds.TarWriter(tar_fname) as sink, open(
+ jsonl_fname, "w", encoding="utf-8"
+ ) as jsonl_f:
+
+ with ThreadPoolExecutor(max_workers=num_threads) as thread_pool:
+ futures = []
+
+ for meta in records:
+ f = thread_pool.submit(process_audio_item, meta, target_sr)
+ futures.append(f)
+
+ for f in as_completed(futures):
+ result = f.result()
+
+ if "error" in result:
+ error_count += 1
+ errors.append(result["error"])
+ continue
+
+ sample, meta = result["ok"]
+ dur = meta.get("audio_duration", 0.0)
+
+ # Duration filtering (based on actual audio_duration computed above)
+ if min_duration is not None and dur < min_duration:
+ filtered_count += 1
+ continue
+ if max_duration is not None and dur >= max_duration:
+ filtered_count += 1
+ continue
+
+ sink.write(sample)
+
+ jsonl_f.write(json.dumps(meta, ensure_ascii=False) + "\n")
+
+ total_duration += dur
+ processed_count += 1
+
+ # Clean up empty shard files
+ if processed_count == 0:
+ for p in (tar_fname, jsonl_fname):
+ if os.path.exists(p):
+ os.remove(p)
+
+ return (
+ shard_idx,
+ processed_count,
+ error_count,
+ filtered_count,
+ total_duration,
+ errors,
+ )
+
+
+def count_lines(path):
+ with open(path, "rb") as f:
+ return sum(buf.count(b"\n") for buf in iter(lambda: f.read(1 << 20), b""))
+
+
+def pack_dataset(
+ input_jsonl,
+ output_dir,
+ samples_per_shard=5000,
+ num_workers=16,
+ target_sr=24000,
+ threads_per_worker=4,
+ shuffle=False,
+ shuffle_seed=None,
+ min_duration=None,
+ max_duration=None,
+):
+ input_path = Path(input_jsonl)
+ output_dir = Path(output_dir)
+ output_tar_dir = output_dir / "audios"
+ output_tar_dir.mkdir(parents=True, exist_ok=True)
+ output_jsonl_dir = output_dir / "txts"
+ output_jsonl_dir.mkdir(parents=True, exist_ok=True)
+
+ output_tar_pattern = str(output_tar_dir / "shard-%06d.tar")
+ output_jsonl_pattern = str(output_jsonl_dir / "shard-%06d.jsonl")
+
+ error_log_path = str(output_dir / "errors.jsonl")
+
+ # Setup error logger
+ error_logger = logging.getLogger("error_log")
+ error_logger.setLevel(logging.ERROR)
+ error_logger.handlers.clear()
+ fh = logging.FileHandler(error_log_path, mode="w", encoding="utf-8")
+ fh.setFormatter(logging.Formatter("%(message)s"))
+ error_logger.addHandler(fh)
+
+ shard_manifest = {}
+
+ print(f"Reading input: {input_path}")
+ print(f"Output dir: {output_dir}")
+ print(f"Strategy: {num_workers} Processes x {threads_per_worker} Threads")
+
+ if shuffle:
+ print("Load input dataset...")
+ entries = list(read_jsonl(input_path))
+ random.seed(shuffle_seed)
+ random.shuffle(entries)
+ print(f"Shuffled {len(entries)} entries (seed={shuffle_seed})")
+ total_lines = len(entries)
+ chunk_gen = chunked_reader(iter(entries), samples_per_shard)
+ else:
+ print("Calculating total lines...")
+ total_lines = count_lines(input_path)
+ chunk_gen = chunked_reader(read_jsonl(input_path), samples_per_shard)
+
+ if min_duration is not None or max_duration is not None:
+ print(
+ f"Duration filter: [{min_duration or 0:.2f}s"
+ f", {max_duration or float('inf'):.1f}s) (applied after audio decoding)"
+ )
+
+ total_shards_est = (total_lines + samples_per_shard - 1) // samples_per_shard
+ print(f"Total samples: {total_lines}, Estimated shards: {total_shards_est}")
+
+ with ProcessPoolExecutor(max_workers=num_workers) as executor:
+
+ futures = set()
+
+ shard_idx = 0
+ total_processed = 0
+ total_errors = 0
+ total_filtered = 0
+
+ pbar = tqdm(
+ total=total_shards_est,
+ desc="Shards Processed",
+ unit="shard",
+ )
+
+ def submit_next_chunks(limit):
+ """Pull up to `limit` chunks from generator, submit them."""
+ nonlocal shard_idx
+ submitted = 0
+ for chunk in chunk_gen:
+ f = executor.submit(
+ process_single_shard,
+ shard_idx,
+ chunk,
+ output_tar_pattern,
+ output_jsonl_pattern,
+ target_sr,
+ threads_per_worker,
+ min_duration,
+ max_duration,
+ )
+ futures.add(f)
+ shard_idx += 1
+ submitted += 1
+ if submitted >= limit:
+ break
+
+ submit_next_chunks(num_workers * 2)
+
+ while futures:
+ done, _ = wait(futures, return_when=FIRST_COMPLETED)
+
+ for f in done:
+ futures.remove(f)
+
+ try:
+ s_idx, p_count, e_count, f_count, s_duration, errors = f.result()
+ total_processed += p_count
+ total_errors += e_count
+ total_filtered += f_count
+
+ # Write error log
+ for err in errors:
+ err["shard_idx"] = s_idx
+ error_logger.error(json.dumps(err, ensure_ascii=False))
+
+ if p_count > 0:
+ tar_abs = os.path.abspath(output_tar_pattern % s_idx)
+ jsonl_abs = os.path.abspath(output_jsonl_pattern % s_idx)
+ shard_manifest[s_idx] = (
+ tar_abs,
+ jsonl_abs,
+ p_count,
+ s_duration,
+ )
+
+ pbar.set_postfix(
+ {
+ "Samples": total_processed,
+ "Filtered": total_filtered,
+ "Errors": total_errors,
+ }
+ )
+ pbar.update(1)
+ except Exception as e:
+ print(f"Shard task failed: {e}")
+
+ submit_next_chunks(1)
+
+ pbar.close()
+
+ # Write final manifest file (data.lst)
+ manifest_path = str(output_dir / "data.lst")
+ with open(manifest_path, "w", encoding="utf-8") as mf:
+ for idx in sorted(shard_manifest.keys()):
+ tar_path, jsonl_path, count, duration = shard_manifest[idx]
+ mf.write(f"{tar_path} {jsonl_path} {count} {duration:.3f}\n")
+
+ print(f"\nDone! Output saved to {output_dir}")
+ print(f"Successfully packed: {total_processed}")
+ print(f"Filtered by duration: {total_filtered}")
+ print(f"Failed: {total_errors}")
+ print(f"Manifest written to: {manifest_path} ({len(shard_manifest)} shards)")
+ if total_errors > 0:
+ print(f"Error details: {error_log_path}")
+
+
+if __name__ == "__main__":
+ mp.set_start_method("spawn", force=True)
+
+ args = build_parser().parse_args()
+ pack_dataset(
+ input_jsonl=args.input,
+ output_dir=args.output,
+ samples_per_shard=args.shard_size,
+ num_workers=args.workers,
+ target_sr=args.sr,
+ threads_per_worker=args.threads,
+ shuffle=args.shuffle,
+ shuffle_seed=args.shuffle_seed,
+ min_duration=args.min_duration,
+ max_duration=args.max_duration,
+ )
diff --git a/omnivoice/training/__init__.py b/omnivoice/training/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/training/builder.py b/omnivoice/training/builder.py
new file mode 100644
index 00000000..28f3c71c
--- /dev/null
+++ b/omnivoice/training/builder.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Builders for constructing training components.
+
+Provides factory functions to assemble the model, tokenizer, and data loaders
+from a ``TrainingConfig``. Called by ``omnivoice.cli.train`` to set up training.
+
+Key functions:
+- ``build_model_and_tokenizer()``: Loads the model and text tokenizer.
+- ``build_dataloaders()``: Builds packed train/eval data loaders
+ from a data config JSON.
+"""
+
+import logging
+from functools import partial
+from typing import Tuple
+
+import torch
+from torch.utils.data import DataLoader
+from transformers import AutoConfig, AutoModel, AutoTokenizer
+from transformers import logging as hf_logging
+from transformers.trainer_utils import seed_worker
+
+from omnivoice.data.batching import PackingIterableDataset
+from omnivoice.data.collator import PackingDataCollator
+from omnivoice.data.dataset import WebDatasetReader, prepare_data_manifests_from_json
+from omnivoice.data.processor import OmniVoiceSampleProcessor
+from omnivoice.models.omnivoice import OmniVoice, OmniVoiceConfig
+from omnivoice.training.config import TrainingConfig
+
+logger = logging.getLogger(__name__)
+
+
+def build_model_and_tokenizer(
+ config: TrainingConfig,
+) -> Tuple[OmniVoice, AutoTokenizer]:
+ """Load Tokenizer and Model, handle resizing and special tokens."""
+ logger.info("Initializing Model & Tokenizer...")
+
+ # 1. Tokenizer
+ tokenizer_path = (
+ config.init_from_checkpoint
+ if config.init_from_checkpoint
+ else config.llm_name_or_path
+ )
+ tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
+ if tokenizer.pad_token is None:
+ tokenizer.pad_token = tokenizer.eos_token
+
+ new_tokens = [
+ "<|denoise|>",
+ "<|lang_start|>",
+ "<|lang_end|>",
+ "<|instruct_start|>",
+ "<|instruct_end|>",
+ "<|text_start|>",
+ "<|text_end|>",
+ ]
+
+ tokens_to_add = [t for t in new_tokens if t not in tokenizer.get_vocab()]
+ if tokens_to_add:
+ tokenizer.add_special_tokens({"additional_special_tokens": tokens_to_add})
+
+ if config.init_from_checkpoint:
+ logger.info(f"Loading weights from {config.init_from_checkpoint}")
+ model = OmniVoice.from_pretrained(
+ config.init_from_checkpoint,
+ attn_implementation="flex_attention",
+ dtype=torch.float32,
+ train=True,
+ )
+ else:
+ llm_config = AutoConfig.from_pretrained(config.llm_name_or_path)
+
+ ov_config = OmniVoiceConfig(
+ audio_vocab_size=config.audio_vocab_size,
+ audio_mask_id=config.audio_mask_id,
+ num_audio_codebook=config.num_audio_codebook,
+ audio_codebook_weights=config.audio_codebook_weights,
+ llm_config=llm_config,
+ )
+
+ original_level = hf_logging.get_verbosity()
+ hf_logging.set_verbosity_error() # suppress expected lm_head.weight warnings
+
+ llm = AutoModel.from_pretrained(
+ config.llm_name_or_path,
+ attn_implementation="flex_attention",
+ dtype=torch.float32,
+ )
+
+ hf_logging.set_verbosity(original_level)
+ model = OmniVoice(config=ov_config, llm=llm)
+
+ # 3. Resize Embeddings
+ if len(tokenizer) != model.config.llm_config.vocab_size:
+ model.llm.resize_token_embeddings(len(tokenizer))
+ model.config.llm_config.vocab_size = len(tokenizer)
+
+ # 4. Config IDs
+ model.config.pad_token_id = tokenizer.pad_token_id
+ model.config.bos_token_id = tokenizer.bos_token_id
+ model.config.eos_token_id = tokenizer.eos_token_id
+
+ return model, tokenizer
+
+
+def build_dataloaders(
+ config: TrainingConfig, tokenizer: AutoTokenizer
+) -> Tuple[DataLoader, DataLoader]:
+ """Setup Data Pipeline: Manifests -> WDS -> Packing -> Loaders."""
+ logger.info("Initializing Data Readers...")
+
+ processor = OmniVoiceSampleProcessor(
+ text_tokenizer=tokenizer,
+ num_channels=config.num_audio_codebook,
+ audio_mask_id=config.audio_mask_id,
+ prompt_ratio_range=config.prompt_ratio_range,
+ mask_ratio_range=config.mask_ratio_range,
+ drop_cond_ratio=config.drop_cond_ratio,
+ language_ratio=config.language_ratio,
+ use_pinyin_ratio=config.use_pinyin_ratio,
+ instruct_ratio=config.instruct_ratio,
+ only_instruct_ratio=config.only_instruct_ratio,
+ )
+
+ train_manifests, dev_manifests = prepare_data_manifests_from_json(
+ config.data_config
+ )
+ raw_train_ds = WebDatasetReader(manifests=train_manifests, evaluation=False)
+
+ train_dataset = PackingIterableDataset(raw_train_ds, processor, config.batch_tokens)
+
+ collate_fn = PackingDataCollator(processor, config.batch_tokens)
+
+ init_fn = partial(
+ seed_worker,
+ num_workers=config.num_workers,
+ rank=torch.distributed.get_rank() if torch.distributed.is_initialized() else 0,
+ )
+
+ train_loader = DataLoader(
+ train_dataset,
+ batch_size=None, # Each item is a batch packed to the target batch_tokens
+ num_workers=config.num_workers,
+ collate_fn=collate_fn,
+ worker_init_fn=init_fn,
+ pin_memory=True,
+ prefetch_factor=4,
+ )
+
+ eval_loader = None
+ if dev_manifests:
+ raw_dev_ds = WebDatasetReader(manifests=dev_manifests, evaluation=True)
+ dev_dataset = PackingIterableDataset(raw_dev_ds, processor, config.batch_tokens)
+ eval_loader = DataLoader(
+ dev_dataset,
+ batch_size=None, # Each item is a batch packed to the target batch_tokens
+ num_workers=1,
+ collate_fn=collate_fn,
+ pin_memory=True,
+ prefetch_factor=2,
+ )
+
+ return train_loader, eval_loader
diff --git a/omnivoice/training/checkpoint.py b/omnivoice/training/checkpoint.py
new file mode 100644
index 00000000..4bbf969d
--- /dev/null
+++ b/omnivoice/training/checkpoint.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Checkpoint saving, resuming, and training logging.
+
+Provides utilities for saving/loading training checkpoints and logging metrics
+to console and trackers (TensorBoard/WandB). Used by ``OmniTrainer``.
+
+Key components:
+- ``TrainLogger``: Logs training metrics to console and Accelerate trackers.
+- ``save_checkpoint()``: Saves model, optimizer, and scheduler state.
+- ``load_checkpoint()``: Restores training state from a checkpoint directory.
+"""
+
+import logging
+import os
+import shutil
+import time
+from typing import Any, Dict, Optional
+
+import torch
+from accelerate import Accelerator
+from tqdm.auto import tqdm
+
+logger = logging.getLogger(__name__)
+
+
+class TrainLogger:
+ """
+ Handles logging to console and trackers (TensorBoard/WandB)
+ """
+
+ def __init__(self, accelerator: Accelerator, total_steps: int, logging_steps: int):
+ self.accelerator = accelerator
+ self.total_steps = total_steps
+ self.logging_steps = logging_steps
+ self.start_time = None
+ self.progress_bar = None
+
+ def start(self, start_step: int = 0):
+ self.start_time = time.time()
+
+ if self.accelerator.is_main_process:
+ self.progress_bar = tqdm(
+ total=self.total_steps,
+ initial=start_step,
+ desc="Training",
+ dynamic_ncols=True,
+ disable=not self.accelerator.is_local_main_process,
+ )
+
+ def update(
+ self, step: int, loss: Optional[float] = None, lr: Optional[float] = None
+ ):
+ """
+ Called every step to update the progress bar UI.
+ """
+ if self.progress_bar:
+ self.progress_bar.update(1)
+
+ # Update real-time metrics on the progress bar itself
+ postfix = {}
+ if loss is not None:
+ postfix["loss"] = f"{loss:.4f}"
+ if lr is not None:
+ postfix["lr"] = f"{lr:.2e}"
+
+ if postfix:
+ self.progress_bar.set_postfix(postfix)
+
+ def log_metrics(self, step: int, metrics: Dict[str, Any]):
+ """
+ Called periodically to log to TensorBoard/WandB and console.
+ """
+ # Log to trackers (TensorBoard, etc.)
+ self.accelerator.log(metrics, step=step)
+
+ if self.accelerator.is_main_process:
+ # Format for console log (separate from tqdm)
+ # Remove keys that are redundant or too verbose for one line
+ formatted_metrics = []
+ for k, v in metrics.items():
+ if isinstance(v, float):
+ val_str = f"{v:.4f}"
+ if val_str == "0.0000" and v != 0:
+ formatted_metrics.append(f"{k}: {v:.2e}")
+ else:
+ formatted_metrics.append(f"{k}: {val_str}")
+ else:
+ formatted_metrics.append(f"{k}: {v}")
+
+ # Use external logger to write to file, tqdm.write to avoid breaking bar
+ msg = f"Step {step} | " + " | ".join(formatted_metrics)
+ if self.progress_bar:
+ self.progress_bar.write(msg)
+ else:
+ logger.info(msg)
+
+ def close(self):
+ if self.progress_bar:
+ self.progress_bar.close()
+
+
+def save_checkpoint(
+ accelerator: Accelerator,
+ model: torch.nn.Module,
+ tokenizer: Any,
+ output_dir: str,
+ step: int,
+ keep_last_n: int = 3,
+):
+ """
+ Saves model, tokenizer, and accelerator states (optimizer/scheduler).
+ Manages rotation of checkpoints.
+ """
+ checkpoint_dir = os.path.join(output_dir, f"checkpoint-{step}")
+
+ # 1. Save Accelerator State (Optimizer, Scheduler, RNG, Scaler)
+ accelerator.save_state(checkpoint_dir)
+
+ # 2. Save Model in HF format (config.json + pytorch_model.bin/safetensors)
+ unwrap_model = accelerator.unwrap_model(model)
+ unwrap_model.save_pretrained(
+ checkpoint_dir,
+ is_main_process=accelerator.is_main_process,
+ save_function=accelerator.save,
+ )
+
+ # 3. Save Tokenizer
+ if accelerator.is_main_process:
+ tokenizer.save_pretrained(checkpoint_dir)
+
+ logger.info(f"Saved checkpoint to {checkpoint_dir}")
+
+ # 4. Rotate checkpoints (Keep last N)
+ if accelerator.is_main_process and keep_last_n > 0:
+ checkpoints = [
+ d
+ for d in os.listdir(output_dir)
+ if d.startswith("checkpoint-")
+ and os.path.isdir(os.path.join(output_dir, d))
+ ]
+ # Sort by step number
+ checkpoints.sort(key=lambda x: int(x.split("-")[-1]))
+
+ if len(checkpoints) > keep_last_n:
+ to_remove = checkpoints[:-keep_last_n]
+ for d in to_remove:
+ shutil.rmtree(os.path.join(output_dir, d))
+ logger.info(f"Removed old checkpoint {d}")
+
+
+def load_checkpoint(accelerator: Accelerator, checkpoint_path: str):
+ """
+ Resumes training state.
+ """
+ logger.info(f"Resuming from {checkpoint_path}")
+ accelerator.load_state(checkpoint_path)
+
+ # Try to infer step
+ try:
+ clean_path = os.path.normpath(checkpoint_path)
+ step = int(os.path.basename(clean_path).split("-")[-1])
+ return step
+ except ValueError:
+ return 0
diff --git a/omnivoice/training/config.py b/omnivoice/training/config.py
new file mode 100644
index 00000000..ecae7259
--- /dev/null
+++ b/omnivoice/training/config.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Training configuration dataclass.
+
+Defines ``TrainingConfig``, a dataclass that holds all hyperparameters and paths
+for training. Loaded from a JSON config file via ``TrainingConfig.from_json()``
+in ``omnivoice.cli.train``.
+"""
+
+import json
+from dataclasses import asdict, dataclass, field
+from typing import List, Optional, Tuple
+
+
+@dataclass
+class TrainingConfig:
+ # Key Paths
+ output_dir: Optional[str] = None
+ data_config: Optional[str] = None
+
+ # Model Specific
+ llm_name_or_path: str = "Qwen/Qwen3-0.6B"
+ audio_vocab_size: int = 1025 # valid vocab size + 1 (mask token)
+ audio_mask_id: int = 1024 # 1024 is the 1025-th token
+ num_audio_codebook: int = 8
+
+ # Model Training Specific
+ audio_codebook_weights: List[float | int] = field(
+ default_factory=lambda: [8, 8, 6, 6, 4, 4, 2, 2]
+ )
+ drop_cond_ratio: float = 0.1
+ prompt_ratio_range: Tuple[float, float] = field(default_factory=lambda: (0.0, 0.3))
+ mask_ratio_range: Tuple[float, float] = field(default_factory=lambda: (0.0, 1.0))
+ language_ratio: float = 0.8
+ use_pinyin_ratio: float = 0.3
+ instruct_ratio: float = 1.0
+ only_instruct_ratio: float = 0.5
+
+ # Init settings
+ resume_from_checkpoint: Optional[str] = None
+ init_from_checkpoint: Optional[str] = None
+
+ # Training Hyperparams
+ learning_rate: float = 1e-4
+ weight_decay: float = 0.01
+ max_grad_norm: float = 1.0
+ steps: int = 300000
+ seed: int = 42
+ lr_scheduler_type: str = "cosine"
+ warmup_type: str = "ratio"
+ warmup_ratio: float = 0.03
+ warmup_steps: int = 2000
+
+ # Data
+ batch_tokens: int = 8192
+ gradient_accumulation_steps: int = 1
+ num_workers: int = 8
+
+ # System
+ mixed_precision: str = "bf16"
+ allow_tf32: bool = True
+ use_deepspeed: bool = False
+ deepspeed_config: Optional[str] = None
+
+ # Logging
+ logging_steps: int = 100
+ eval_steps: int = 1000
+ save_steps: int = 10000
+ keep_last_n_checkpoints: int = -1
+
+ @classmethod
+ def from_json(cls, json_path: str):
+ with open(json_path, "r") as f:
+ cfg_dict = json.load(f)
+ valid_keys = cls.__annotations__.keys()
+ filtered_dict = {k: v for k, v in cfg_dict.items() if k in valid_keys}
+ instance = cls(**filtered_dict)
+ return instance
+
+ def save_to_json(self, json_path: str):
+ data = asdict(self)
+ with open(json_path, "w") as f:
+ json.dump(data, f, indent=4)
diff --git a/omnivoice/training/trainer.py b/omnivoice/training/trainer.py
new file mode 100644
index 00000000..081955ea
--- /dev/null
+++ b/omnivoice/training/trainer.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Training loop for OmniVoice.
+
+Wraps the HuggingFace Accelerate training loop with checkpoint saving/resuming,
+evaluation, gradient accumulation, and learning rate scheduling.
+Launched via ``omnivoice.cli.train``.
+"""
+
+import logging
+import math
+import os
+import sys
+import time
+from datetime import timedelta
+from typing import Any, Optional
+
+import torch
+from accelerate import Accelerator, DistributedDataParallelKwargs
+from accelerate.utils import DeepSpeedPlugin, InitProcessGroupKwargs, set_seed
+from torch.utils.data import DataLoader
+from transformers import (
+ get_cosine_schedule_with_warmup,
+ get_constant_schedule_with_warmup,
+)
+
+from omnivoice.training.checkpoint import TrainLogger, load_checkpoint
+from omnivoice.training.checkpoint import save_checkpoint as engine_save_checkpoint
+
+logger = logging.getLogger(__name__)
+
+
+def _to_device(batch, device):
+ """Move all tensors in a batch dict to the target device."""
+ return {
+ k: v.to(device, non_blocking=True) if isinstance(v, torch.Tensor) else v
+ for k, v in batch.items()
+ }
+
+
+class OmniTrainer:
+ def __init__(
+ self,
+ model: torch.nn.Module,
+ config: Any, # TrainingConfig
+ train_dataloader: DataLoader,
+ eval_dataloader: Optional[DataLoader] = None,
+ tokenizer: Optional[Any] = None,
+ optimizer: Optional[torch.optim.Optimizer] = None,
+ lr_scheduler: Optional[Any] = None,
+ ):
+ self.config = config
+ self.model = model
+ self.tokenizer = tokenizer
+ self.train_dataloader = train_dataloader
+ self.eval_dataloader = eval_dataloader
+
+ # 1. Initialize Accelerator
+ self.accelerator = self._init_accelerator()
+
+ # 2. Setup Optimizer & Scheduler if not provided
+ if optimizer is None:
+ self.optimizer, self.lr_scheduler = self.create_optimizer_and_scheduler()
+ else:
+ self.optimizer = optimizer
+ self.lr_scheduler = lr_scheduler
+
+ # 3. DeepSpeed Hack (Batch Size fix)
+ if self.accelerator.distributed_type == "DEEPSPEED":
+ self.accelerator.state.deepspeed_plugin.deepspeed_config[
+ "train_micro_batch_size_per_gpu"
+ ] = 1
+
+ # 4. Prepare with Accelerator
+ (self.model, self.optimizer, self.lr_scheduler,) = self.accelerator.prepare(
+ self.model,
+ self.optimizer,
+ self.lr_scheduler,
+ )
+
+ self.global_step = 0
+ self.epoch = 0
+
+ def _init_accelerator(self) -> Accelerator:
+ """Initialize Accelerator, DeepSpeed, and Logging."""
+ # TF32 setup
+ if getattr(self.config, "allow_tf32", False):
+ torch.set_float32_matmul_precision("high")
+
+ # Init handlers
+ ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=False)
+ init_kwargs = InitProcessGroupKwargs(timeout=timedelta(minutes=60))
+
+ # DeepSpeed setup
+ deepspeed_plugin = None
+ if self.config.use_deepspeed and self.config.deepspeed_config:
+ if not os.path.exists(self.config.deepspeed_config):
+ raise FileNotFoundError(
+ f"DeepSpeed config not found: {self.config.deepspeed_config}"
+ )
+ deepspeed_plugin = DeepSpeedPlugin(
+ hf_ds_config=self.config.deepspeed_config,
+ gradient_accumulation_steps=self.config.gradient_accumulation_steps,
+ gradient_clipping=self.config.max_grad_norm,
+ )
+
+ accelerator = Accelerator(
+ gradient_accumulation_steps=self.config.gradient_accumulation_steps,
+ mixed_precision=self.config.mixed_precision,
+ log_with="tensorboard",
+ project_dir=self.config.output_dir,
+ step_scheduler_with_optimizer=False,
+ kwargs_handlers=[ddp_kwargs, init_kwargs],
+ deepspeed_plugin=deepspeed_plugin,
+ split_batches=False,
+ )
+
+ # Logging setup
+ if accelerator.is_main_process:
+ os.makedirs(self.config.output_dir, exist_ok=True)
+ # Try to save config if it has the method
+ if hasattr(self.config, "save_to_json"):
+ self.config.save_to_json(
+ os.path.join(self.config.output_dir, "initial_config.json")
+ )
+
+ logging.basicConfig(
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
+ datefmt="%m/%d/%Y %H:%M:%S",
+ level=logging.INFO,
+ handlers=[
+ logging.StreamHandler(sys.stdout),
+ logging.FileHandler(
+ os.path.join(self.config.output_dir, "train.log")
+ ),
+ ],
+ )
+ else:
+ logging.basicConfig(level=logging.ERROR)
+
+ logger.info(f"Loaded Config: {self.config}")
+ set_seed(self.config.seed)
+ accelerator.init_trackers("tensorboard")
+ return accelerator
+
+ def create_optimizer_and_scheduler(self):
+ """Default AdamW + configurable LR Scheduler."""
+ optimizer = torch.optim.AdamW(
+ self.model.parameters(),
+ lr=self.config.learning_rate,
+ weight_decay=self.config.weight_decay,
+ )
+
+ if self.config.warmup_type == "ratio":
+ final_warmup_steps = math.ceil(self.config.steps * self.config.warmup_ratio)
+ else:
+ final_warmup_steps = self.config.warmup_steps
+
+ if self.config.lr_scheduler_type == "constant":
+ lr_scheduler = get_constant_schedule_with_warmup(
+ optimizer=optimizer,
+ num_warmup_steps=final_warmup_steps,
+ )
+ else:
+ lr_scheduler = get_cosine_schedule_with_warmup(
+ optimizer=optimizer,
+ num_warmup_steps=final_warmup_steps,
+ num_training_steps=self.config.steps,
+ )
+ return optimizer, lr_scheduler
+
+ def save_checkpoint(self, step):
+ """Wrapper for engine save_checkpoint."""
+ engine_save_checkpoint(
+ self.accelerator,
+ self.model,
+ self.tokenizer,
+ self.config.output_dir,
+ step,
+ self.config.keep_last_n_checkpoints,
+ )
+ # Save config copy for convenience
+ if self.accelerator.is_main_process and hasattr(self.config, "save_to_json"):
+ checkpoint_dir = os.path.join(self.config.output_dir, f"checkpoint-{step}")
+ self.config.save_to_json(os.path.join(checkpoint_dir, "train_config.json"))
+
+ def load_checkpoint(self, checkpoint_path):
+ """Wrapper for loading."""
+ step = load_checkpoint(self.accelerator, checkpoint_path)
+ self.global_step = step
+ logger.info(f"Resumed from step {self.global_step}")
+ return step
+
+ def evaluate(self):
+ """Evaluation loop."""
+ if self.eval_dataloader is None:
+ return {}
+
+ self.model.eval()
+ logger.info(f"Running evaluation at step {self.global_step}...")
+
+ local_loss_sum = torch.tensor(0.0, device=self.accelerator.device)
+ eval_count = 0
+
+ with torch.no_grad():
+ for eval_batch in self.eval_dataloader:
+ eval_batch = _to_device(eval_batch, self.accelerator.device)
+ outputs = self.model(**eval_batch)
+ local_loss_sum += outputs.loss.detach()
+ eval_count += 1
+
+ if eval_count > 0:
+ local_mean = local_loss_sum / eval_count
+ else:
+ local_mean = torch.tensor(0.0, device=self.accelerator.device)
+
+ all_means = self.accelerator.gather(local_mean)
+ final_eval_loss = all_means.mean().item()
+
+ eval_metrics = {"eval/loss": final_eval_loss}
+ self.accelerator.log(eval_metrics, step=self.global_step)
+ logger.info(f"Eval Loss: {final_eval_loss:.4f}")
+
+ self.accelerator.wait_for_everyone()
+ self.model.train()
+ return eval_metrics
+
+ def train(self):
+ """Main training loop."""
+ logger.info("Starting Training Loop...")
+
+ # Resume if configured
+ if self.config.resume_from_checkpoint:
+ self.load_checkpoint(self.config.resume_from_checkpoint)
+
+ # Handle IterableDataset Epochs
+ if hasattr(self.train_dataloader.dataset, "set_epoch"):
+ self.train_dataloader.dataset.set_epoch(self.epoch)
+
+ # Logger
+ train_logger = TrainLogger(
+ self.accelerator, self.config.steps, self.config.logging_steps
+ )
+ train_logger.start(self.global_step)
+
+ self.model.train()
+ train_iterator = iter(self.train_dataloader)
+
+ logging_start_time = time.time()
+ logging_start_step = self.global_step
+ tr_loss = torch.tensor(0.0).to(self.accelerator.device)
+ logging_loss_scalar = 0.0
+
+ while self.global_step < self.config.steps:
+ try:
+ batch = next(train_iterator)
+ except StopIteration:
+ self.epoch += 1
+ logger.info(f"Epoch {self.epoch} starting. Resetting dataloader...")
+ if hasattr(self.train_dataloader.dataset, "set_epoch"):
+ self.train_dataloader.dataset.set_epoch(self.epoch)
+
+ train_iterator = iter(self.train_dataloader)
+ batch = next(train_iterator)
+
+ batch = _to_device(batch, self.accelerator.device)
+
+ with self.accelerator.accumulate(self.model):
+ outputs = self.model(**batch)
+ loss = outputs.loss
+ tr_loss += loss.detach()
+ self.accelerator.backward(loss)
+
+ if self.accelerator.sync_gradients:
+ # Clipping
+ grad_norm = 0.0
+ if self.config.max_grad_norm > 0:
+ grad_norm = self.accelerator.clip_grad_norm_(
+ self.model.parameters(), self.config.max_grad_norm
+ )
+ grad_norm = (
+ grad_norm.item() if grad_norm is not None else 0.0
+ )
+
+ self.optimizer.step()
+ self.lr_scheduler.step()
+ self.optimizer.zero_grad()
+ self.global_step += 1
+
+ # Logging
+ current_lr = self.lr_scheduler.get_last_lr()[0]
+ train_logger.update(
+ step=self.global_step, loss=loss.item(), lr=current_lr
+ )
+
+ if self.global_step % self.config.logging_steps == 0:
+ elapsed = time.time() - logging_start_time
+ steps_per_sec = (
+ (self.global_step - logging_start_step) / elapsed
+ if elapsed > 0
+ else 0
+ )
+
+ tr_loss_scalar = self.accelerator.gather(tr_loss).mean().item()
+ current_interval_loss = tr_loss_scalar - logging_loss_scalar
+ avg_loss = current_interval_loss / (
+ self.config.logging_steps
+ * self.config.gradient_accumulation_steps
+ )
+ logging_loss_scalar = tr_loss_scalar
+
+ logs = {
+ "train/loss": avg_loss,
+ "train/learning_rate": current_lr,
+ "train/grad_norm": grad_norm,
+ "train/epoch": self.epoch,
+ "train/steps_per_sec": steps_per_sec,
+ }
+ train_logger.log_metrics(step=self.global_step, metrics=logs)
+
+ logging_start_time = time.time()
+ logging_start_step = self.global_step
+
+ # Evaluate
+ if (
+ self.eval_dataloader is not None
+ and self.global_step % self.config.eval_steps == 0
+ ):
+ self.evaluate()
+
+ # Save
+ if self.global_step % self.config.save_steps == 0:
+ self.save_checkpoint(self.global_step)
+
+ # Final Save
+ self.save_checkpoint(self.global_step)
+ train_logger.close()
+ self.accelerator.end_training()
diff --git a/omnivoice/utils/__init__.py b/omnivoice/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/omnivoice/utils/audio.py b/omnivoice/utils/audio.py
new file mode 100644
index 00000000..9fcb2c2c
--- /dev/null
+++ b/omnivoice/utils/audio.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Audio I/O and processing utilities.
+
+Provides functions for loading, resampling, silence removal, chunking,
+cross-fading, and format conversion. Used by ``OmniVoice.generate()`` during
+inference post-processing.
+"""
+
+import numpy as np
+import torch
+import torchaudio
+from pydub import AudioSegment
+from pydub.silence import detect_leading_silence, detect_nonsilent, split_on_silence
+
+
+def load_audio(audio_path: str, sampling_rate: int):
+ """
+ Load the waveform with torchaudio and resampling if needed.
+
+ Parameters:
+ audio_path: path of the audio.
+ sampling_rate: target sampling rate.
+
+ Returns:
+ Loaded prompt waveform with target sampling rate,
+ PyTorch tensor of shape (1, T)
+ """
+ try:
+ waveform, prompt_sampling_rate = torchaudio.load(
+ audio_path, backend="soundfile"
+ )
+ except (RuntimeError, OSError):
+ # Fallback via pydub+ffmpeg for formats torchaudio can't handle
+ aseg = AudioSegment.from_file(audio_path)
+ audio_data = np.array(aseg.get_array_of_samples()).astype(np.float32) / 32768.0
+ if aseg.channels == 1:
+ waveform = torch.from_numpy(audio_data).unsqueeze(0)
+ else:
+ waveform = torch.from_numpy(audio_data.reshape(-1, aseg.channels).T)
+ prompt_sampling_rate = aseg.frame_rate
+
+ if prompt_sampling_rate != sampling_rate:
+ waveform = torchaudio.functional.resample(
+ waveform,
+ orig_freq=prompt_sampling_rate,
+ new_freq=sampling_rate,
+ )
+ if waveform.shape[0] > 1:
+ waveform = torch.mean(waveform, dim=0, keepdim=True)
+
+ return waveform
+
+
+def remove_silence(
+ audio: torch.Tensor,
+ sampling_rate: int,
+ mid_sil: int = 300,
+ lead_sil: int = 100,
+ trail_sil: int = 300,
+):
+ """
+ Remove middle silences longer than mid_sil ms, and edge silences longer than edge_sil ms
+
+ Parameters:
+ audio: PyTorch tensor with shape (C, T).
+ sampling_rate: sampling rate of the audio.
+ mid_sil: the duration of silences in the middle of audio to be removed in ms.
+ if mid_sil <= 0, no middle silence will be removed.
+ edge_sil: the duration of silences in the edge of audio to be removed in ms.
+ trail_sil: the duration of added trailing silence in ms.
+
+ Returns:
+ PyTorch tensor with shape (C, T), where C is number of channels
+ and T is number of audio samples
+ """
+ # Load audio file
+ wave = tensor_to_audiosegment(audio, sampling_rate)
+
+ if mid_sil > 0:
+ # Split audio using silences longer than mid_sil
+ non_silent_segs = split_on_silence(
+ wave,
+ min_silence_len=mid_sil,
+ silence_thresh=-50,
+ keep_silence=mid_sil,
+ seek_step=10,
+ )
+
+ # Concatenate all non-silent segments
+ wave = AudioSegment.silent(duration=0)
+ for seg in non_silent_segs:
+ wave += seg
+
+ # Remove silence longer than 0.1 seconds in the begining and ending of wave
+ wave = remove_silence_edges(wave, lead_sil, trail_sil, -50)
+
+ # Convert to PyTorch tensor
+ return audiosegment_to_tensor(wave)
+
+
+def remove_silence_edges(
+ audio: AudioSegment,
+ lead_sil: int = 100,
+ trail_sil: int = 300,
+ silence_threshold: float = -50,
+):
+ """
+ Remove edge silences longer than `keep_silence` ms.
+
+ Parameters:
+ audio: an AudioSegment object.
+ keep_silence: kept silence in the edge.
+ only_edge: If true, only remove edge silences.
+ silence_threshold: the threshold of silence.
+
+ Returns:
+ An AudioSegment object
+ """
+ # Remove heading silence
+ start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold)
+ start_idx = max(0, start_idx - lead_sil)
+ audio = audio[start_idx:]
+
+ # Remove trailing silence
+ audio = audio.reverse()
+ start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold)
+ start_idx = max(0, start_idx - trail_sil)
+ audio = audio[start_idx:]
+ audio = audio.reverse()
+
+ return audio
+
+
+def audiosegment_to_tensor(aseg):
+ """
+ Convert a pydub.AudioSegment to PyTorch audio tensor
+ """
+ audio_data = np.array(aseg.get_array_of_samples())
+
+ # Convert to float32 and normalize to [-1, 1] range
+ audio_data = audio_data.astype(np.float32) / 32768.0
+
+ # Handle channels
+ if aseg.channels == 1:
+ # Mono channel: add channel dimension (T) -> (1, T)
+ tensor_data = torch.from_numpy(audio_data).unsqueeze(0)
+ else:
+ # Multi-channel: reshape to (C, T)
+ tensor_data = torch.from_numpy(audio_data.reshape(-1, aseg.channels).T)
+
+ return tensor_data
+
+
+def tensor_to_audiosegment(tensor, sample_rate):
+ """
+ Convert a PyTorch audio tensor to pydub.AudioSegment
+
+ Parameters:
+ tensor: Tensor with shape (C, T), where C is the number of channels
+ and T is the time steps
+ sample_rate: Audio sample rate
+ """
+ # Convert tensor to numpy array
+ assert isinstance(tensor, torch.Tensor)
+ audio_np = tensor.cpu().numpy()
+
+ # Convert to int16 type (common format for pydub)
+ # Assumes tensor values are in [-1, 1] range as floating point
+ audio_np = (audio_np * 32768.0).clip(-32768, 32767).astype(np.int16)
+
+ # Convert to byte stream
+ # For multi-channel audio, pydub requires interleaved format
+ # (e.g., left-right-left-right)
+ if audio_np.shape[0] > 1:
+ # Convert to interleaved format
+ audio_np = audio_np.transpose(1, 0).flatten()
+ audio_bytes = audio_np.tobytes()
+
+ # Create AudioSegment
+ audio_segment = AudioSegment(
+ data=audio_bytes,
+ sample_width=2,
+ frame_rate=sample_rate,
+ channels=tensor.shape[0],
+ )
+
+ return audio_segment
+
+
+def fade_and_pad_audio(
+ audio: torch.Tensor,
+ pad_duration: float = 0.1,
+ fade_duration: float = 0.1,
+ sample_rate: int = 24000,
+) -> torch.Tensor:
+ """
+ Applies a smooth fade-in and fade-out to the audio, and then pads both sides
+ with pure silence to prevent abrupt starts and ends (clicks/pops).
+
+ Args:
+ audio: PyTorch tensor of shape (C, T) containing audio data.
+ pad_duration: Duration of pure silence to add to each end (in seconds).
+ fade_duration: Duration of the fade-in/out curve (in seconds).
+ sample_rate: Audio sampling rate.
+
+ Returns:
+ Processed sequence tensor with shape (C, T_new)
+ """
+ if audio.shape[-1] == 0:
+ return audio
+
+ fade_samples = int(fade_duration * sample_rate)
+ pad_samples = int(pad_duration * sample_rate)
+
+ processed = audio.clone()
+
+ if fade_samples > 0:
+ k = min(fade_samples, processed.shape[-1] // 2)
+
+ if k > 0:
+ fade_in = torch.linspace(
+ 0, 1, k, device=processed.device, dtype=processed.dtype
+ )[None, :]
+ processed[..., :k] = processed[..., :k] * fade_in
+
+ fade_out = torch.linspace(
+ 1, 0, k, device=processed.device, dtype=processed.dtype
+ )[None, :]
+ processed[..., -k:] = processed[..., -k:] * fade_out
+
+ if pad_samples > 0:
+ silence = torch.zeros(
+ (processed.shape[0], pad_samples),
+ dtype=processed.dtype,
+ device=processed.device,
+ )
+ processed = torch.cat([silence, processed, silence], dim=-1)
+
+ return processed
+
+
+def trim_long_audio(
+ audio: torch.Tensor,
+ sampling_rate: int,
+ max_duration: float = 15.0,
+ min_duration: float = 3.0,
+ trim_threshold: float = 20.0,
+) -> torch.Tensor:
+ """Trim audio to <= max_duration by splitting at the largest silence gap.
+
+ Only trims when the audio exceeds *trim_threshold* seconds.
+
+ Args:
+ audio: Audio tensor of shape (C, T).
+ sampling_rate: Audio sampling rate.
+ max_duration: Maximum duration in seconds.
+ min_duration: Minimum duration in seconds.
+ trim_threshold: Only trim if audio is longer than this (seconds).
+
+ Returns:
+ Trimmed audio tensor.
+ """
+ duration = audio.size(-1) / sampling_rate
+ if duration <= trim_threshold:
+ return audio
+
+ seg = tensor_to_audiosegment(audio, sampling_rate)
+ nonsilent = detect_nonsilent(
+ seg, min_silence_len=100, silence_thresh=-40, seek_step=10
+ )
+ if not nonsilent:
+ return audio
+
+ max_ms = int(max_duration * 1000)
+ min_ms = int(min_duration * 1000)
+
+ # Walk through speech regions; at each gap pick the latest split <= max_duration
+ best_split = 0
+ for start, end in nonsilent:
+ if start > best_split and start <= max_ms:
+ best_split = start
+ if end > max_ms:
+ break
+
+ if best_split < min_ms:
+ best_split = min(max_ms, len(seg))
+
+ trimmed = seg[:best_split]
+ return audiosegment_to_tensor(trimmed)
+
+
+def cross_fade_chunks(
+ chunks: list[torch.Tensor],
+ sample_rate: int,
+ silence_duration: float = 0.3,
+) -> torch.Tensor:
+ """Concatenate audio chunks with a short silence gap and fade at boundaries.
+
+ Each boundary is structured as: fade-out tail → silence buffer → fade-in head.
+ This avoids click artifacts from direct concatenation or overlapping mismatch.
+
+ Args:
+ chunks: List of audio tensors, each (C, T).
+ sample_rate: Audio sample rate.
+ silence_duration: Total silence gap duration in seconds.
+
+ Returns:
+ Merged audio tensor (C, T_total).
+ """
+ if len(chunks) == 1:
+ return chunks[0]
+
+ total_n = int(silence_duration * sample_rate)
+ fade_n = total_n // 3
+ silence_n = fade_n # middle silent gap
+ merged = chunks[0].clone()
+
+ for chunk in chunks[1:]:
+ dev, dt = merged.device, merged.dtype
+ parts = [merged]
+
+ # Fade out tail of current merged audio
+ fout_n = min(fade_n, merged.size(-1))
+ if fout_n > 0:
+ w_out = torch.linspace(1, 0, fout_n, device=dev, dtype=dt)[None, :]
+ parts[-1][..., -fout_n:] = parts[-1][..., -fout_n:] * w_out
+
+ # Silent buffer between chunks
+ parts.append(torch.zeros(chunks[0].shape[0], silence_n, device=dev, dtype=dt))
+
+ # Fade in head of next chunk
+ fade_in = chunk.clone()
+ fin_n = min(fade_n, fade_in.size(-1))
+ if fin_n > 0:
+ w_in = torch.linspace(0, 1, fin_n, device=dev, dtype=dt)[None, :]
+ fade_in[..., :fin_n] = fade_in[..., :fin_n] * w_in
+
+ parts.append(fade_in)
+ merged = torch.cat(parts, dim=-1)
+
+ return merged
diff --git a/omnivoice/utils/common.py b/omnivoice/utils/common.py
new file mode 100644
index 00000000..6bdd3426
--- /dev/null
+++ b/omnivoice/utils/common.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Shared utility functions."""
+
+import argparse
+import random
+
+import numpy as np
+import torch
+
+
+def str2bool(v):
+ """Used in argparse.ArgumentParser.add_argument to indicate
+ that a type is a bool type and user can enter
+
+ - yes, true, t, y, 1, to represent True
+ - no, false, f, n, 0, to represent False
+
+ See https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse # noqa
+ """
+ if isinstance(v, bool):
+ return v
+ if v.lower() in ("yes", "true", "t", "y", "1"):
+ return True
+ elif v.lower() in ("no", "false", "f", "n", "0"):
+ return False
+ else:
+ raise argparse.ArgumentTypeError("Boolean value expected.")
+
+
+def fix_random_seed(random_seed: int):
+ """
+ Set the same random seed for the libraries and modules.
+ Includes the ``random`` module, numpy, and torch.
+ """
+ random.seed(random_seed)
+ np.random.seed(random_seed)
+ torch.random.manual_seed(random_seed)
+ # Ensure deterministic ID creation
+ rd = random.Random()
+ rd.seed(random_seed)
diff --git a/omnivoice/utils/data_utils.py b/omnivoice/utils/data_utils.py
new file mode 100644
index 00000000..075be641
--- /dev/null
+++ b/omnivoice/utils/data_utils.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Data utilities for batch inference and evaluation.
+
+Provides ``read_test_list()`` to parse JSONL test list files used by
+``omnivoice.cli.infer_batch`` and evaluation scripts.
+"""
+
+import json
+import logging
+from pathlib import Path
+
+
+def read_test_list(path):
+ """Read a JSONL test list file.
+
+ Each line should be a JSON object with fields:
+ id, text, ref_audio, ref_text, language_id, language_name, duration, speed
+
+ language_id, language_name, duration, and speed are optional (default to None).
+
+ Returns a list of dicts.
+ """
+ path = Path(path)
+ samples = []
+ with path.open("r", encoding="utf-8") as f:
+ for line_no, line in enumerate(f, 1):
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ obj = json.loads(line)
+ except json.JSONDecodeError:
+ logging.warning(f"Skipping malformed JSON at line {line_no}: {line}")
+ continue
+
+ sample = {
+ "id": obj.get("id"),
+ "text": obj.get("text"),
+ "ref_audio": obj.get("ref_audio"),
+ "ref_text": obj.get("ref_text"),
+ "language_id": obj.get("language_id"),
+ "language_name": obj.get("language_name"),
+ "duration": obj.get("duration"),
+ "speed": obj.get("speed"),
+ "instruct": obj.get("instruct"),
+ }
+ samples.append(sample)
+ return samples
diff --git a/omnivoice/utils/duration.py b/omnivoice/utils/duration.py
new file mode 100644
index 00000000..f3bb9d76
--- /dev/null
+++ b/omnivoice/utils/duration.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Text duration estimation for TTS generation.
+
+Provides ``RuleDurationEstimator``, which estimates audio duration from text
+using character phonetic weights across 600+ languages. Used by
+``OmniVoice.generate()`` to determine output length when no duration is specified.
+"""
+
+import bisect
+import unicodedata
+from functools import lru_cache
+from typing import Optional
+
+
+class RuleDurationEstimator:
+ def __init__(self):
+ # ==========================================
+ # 1. Phonetic Weights Table
+ # ==========================================
+ # The weight represents the relative speaking time compared to
+ # a standard Latin letter.
+ # Benchmark: 1.0 = One Latin Character (~40-50ms)
+ self.weights = {
+ # --- Logographic (1 char = full syllable/word) ---
+ "cjk": 3.0, # Chinese, Japanese Kanji, etc.
+ # --- Syllabic / Blocks
+ "hangul": 2.5, # Korean Hangul
+ "kana": 2.2, # Japanese Hiragana/Katakana
+ "ethiopic": 3.0, # Amharic/Ge'ez
+ "yi": 3.0, # Yi script
+ # --- Abugida (Consonant-Vowel complexes) ---
+ "indic": 1.8, # Hindi, Bengali, Tamil, etc.
+ "thai_lao": 1.5, # Thai, Lao
+ "khmer_myanmar": 1.8, # Khmer, Myanmar
+ # --- Abjad (Consonant-heavy) ---
+ "arabic": 1.5, # Arabic, Persian, Urdu
+ "hebrew": 1.5, # Hebrew
+ # --- Alphabet (Segmental) ---
+ "latin": 1.0, # English, Spanish, French, Vietnamese, etc. (Baseline)
+ "cyrillic": 1.0, # Russian, Ukrainian
+ "greek": 1.0, # Greek
+ "armenian": 1.0, # Armenian
+ "georgian": 1.0, # Georgian
+ # --- Symbols & Misc ---
+ "punctuation": 0.5, # Pause capability
+ "space": 0.2, # Word boundary/Breath (0.05 / 0.22)
+ "digit": 3.5, # Numbers
+ "mark": 0.0, # Diacritics/Accents (Silent modifiers)
+ "default": 1.0, # Fallback for unknown scripts
+ }
+
+ # ==========================================
+ # 2. Unicode Range Mapping
+ # ==========================================
+ # Format: (End_Codepoint, Type_Key)
+ # Used for fast binary search (bisect).
+ self.ranges = [
+ (0x02AF, "latin"), # Latin (Basic, Supplement, Ext, IPA)
+ (0x03FF, "greek"), # Greek & Coptic
+ (0x052F, "cyrillic"), # Cyrillic
+ (0x058F, "armenian"), # Armenian
+ (0x05FF, "hebrew"), # Hebrew
+ (0x077F, "arabic"), # Arabic, Syriac, Arabic Supplement
+ (0x089F, "arabic"), # Arabic Extended-B (+ Syriac Supp)
+ (0x08FF, "arabic"), # Arabic Extended-A
+ (0x097F, "indic"), # Devanagari
+ (0x09FF, "indic"), # Bengali
+ (0x0A7F, "indic"), # Gurmukhi
+ (0x0AFF, "indic"), # Gujarati
+ (0x0B7F, "indic"), # Oriya
+ (0x0BFF, "indic"), # Tamil
+ (0x0C7F, "indic"), # Telugu
+ (0x0CFF, "indic"), # Kannada
+ (0x0D7F, "indic"), # Malayalam
+ (0x0DFF, "indic"), # Sinhala
+ (0x0EFF, "thai_lao"), # Thai & Lao
+ (0x0FFF, "indic"), # Tibetan (Abugida)
+ (0x109F, "khmer_myanmar"), # Myanmar
+ (0x10FF, "georgian"), # Georgian
+ (0x11FF, "hangul"), # Hangul Jamo
+ (0x137F, "ethiopic"), # Ethiopic
+ (0x139F, "ethiopic"), # Ethiopic Supplement
+ (0x13FF, "default"), # Cherokee
+ (0x167F, "default"), # Canadian Aboriginal Syllabics
+ (0x169F, "default"), # Ogham
+ (0x16FF, "default"), # Runic
+ (0x171F, "default"), # Tagalog (Baybayin)
+ (0x173F, "default"), # Hanunoo
+ (0x175F, "default"), # Buhid
+ (0x177F, "default"), # Tagbanwa
+ (0x17FF, "khmer_myanmar"), # Khmer
+ (0x18AF, "default"), # Mongolian
+ (0x18FF, "default"), # Canadian Aboriginal Syllabics Ext
+ (0x194F, "indic"), # Limbu
+ (0x19DF, "indic"), # Tai Le & New Tai Lue
+ (0x19FF, "khmer_myanmar"), # Khmer Symbols
+ (0x1A1F, "indic"), # Buginese
+ (0x1AAF, "indic"), # Tai Tham
+ (0x1B7F, "indic"), # Balinese
+ (0x1BBF, "indic"), # Sundanese
+ (0x1BFF, "indic"), # Batak
+ (0x1C4F, "indic"), # Lepcha
+ (0x1C7F, "indic"), # Ol Chiki (Santali)
+ (0x1C8F, "cyrillic"), # Cyrillic Extended-C
+ (0x1CBF, "georgian"), # Georgian Extended
+ (0x1CCF, "indic"), # Sundanese Supplement
+ (0x1CFF, "indic"), # Vedic Extensions
+ (0x1D7F, "latin"), # Phonetic Extensions
+ (0x1DBF, "latin"), # Phonetic Extensions Supplement
+ (0x1DFF, "default"), # Combining Diacritical Marks Supplement
+ (0x1EFF, "latin"), # Latin Extended Additional (Vietnamese)
+ (0x309F, "kana"), # Hiragana
+ (0x30FF, "kana"), # Katakana
+ (0x312F, "cjk"), # Bopomofo (Pinyin)
+ (0x318F, "hangul"), # Hangul Compatibility Jamo
+ (0x9FFF, "cjk"), # CJK Unified Ideographs (Main)
+ (0xA4CF, "yi"), # Yi Syllables
+ (0xA4FF, "default"), # Lisu
+ (0xA63F, "default"), # Vai
+ (0xA69F, "cyrillic"), # Cyrillic Extended-B
+ (0xA6FF, "default"), # Bamum
+ (0xA7FF, "latin"), # Latin Extended-D
+ (0xA82F, "indic"), # Syloti Nagri
+ (0xA87F, "default"), # Phags-pa
+ (0xA8DF, "indic"), # Saurashtra
+ (0xA8FF, "indic"), # Devanagari Extended
+ (0xA92F, "indic"), # Kayah Li
+ (0xA95F, "indic"), # Rejang
+ (0xA97F, "hangul"), # Hangul Jamo Extended-A
+ (0xA9DF, "indic"), # Javanese
+ (0xA9FF, "khmer_myanmar"), # Myanmar Extended-B
+ (0xAA5F, "indic"), # Cham
+ (0xAA7F, "khmer_myanmar"), # Myanmar Extended-A
+ (0xAADF, "indic"), # Tai Viet
+ (0xAAFF, "indic"), # Meetei Mayek Extensions
+ (0xAB2F, "ethiopic"), # Ethiopic Extended-A
+ (0xAB6F, "latin"), # Latin Extended-E
+ (0xABBF, "default"), # Cherokee Supplement
+ (0xABFF, "indic"), # Meetei Mayek
+ (0xD7AF, "hangul"), # Hangul Syllables
+ (0xFAFF, "cjk"), # CJK Compatibility
+ (0xFDFF, "arabic"), # Arabic Presentation Forms-A
+ (0xFE6F, "default"), # Variation Selectors
+ (0xFEFF, "arabic"), # Arabic Presentation Forms-B
+ (0xFFEF, "latin"), # Fullwidth Latin
+ ]
+ self.breakpoints = [r[0] for r in self.ranges]
+
+ @lru_cache(maxsize=4096)
+ def _get_char_weight(self, char):
+ """Determines the weight of a single character."""
+ code = ord(char)
+ if (65 <= code <= 90) or (97 <= code <= 122):
+ return self.weights["latin"]
+ if code == 32:
+ return self.weights["space"]
+
+ # Ignore arabic Tatweel
+ if code == 0x0640:
+ return self.weights["mark"]
+
+ category = unicodedata.category(char)
+
+ if category.startswith("M"):
+ return self.weights["mark"]
+
+ if category.startswith("P") or category.startswith("S"):
+ return self.weights["punctuation"]
+
+ if category.startswith("Z"):
+ return self.weights["space"]
+
+ if category.startswith("N"):
+ return self.weights["digit"]
+
+ # 3. Binary search for Unicode Block (此时区间里绝不会再混进标点符号)
+ idx = bisect.bisect_left(self.breakpoints, code)
+ if idx < len(self.ranges):
+ script_type = self.ranges[idx][1]
+ return self.weights.get(script_type, self.weights["default"])
+
+ # 4. Handle upper planes (CJK Ext B/C/D, Historic scripts)
+ if code > 0x20000:
+ return self.weights["cjk"]
+
+ return self.weights["default"]
+
+ def calculate_total_weight(self, text):
+ """Sums up the normalized weights for a string."""
+ return sum(self._get_char_weight(c) for c in text)
+
+ def estimate_duration(
+ self,
+ target_text: str,
+ ref_text: str,
+ ref_duration: float,
+ low_threshold: Optional[float] = 50,
+ boost_strength: float = 3,
+ ) -> float:
+ """
+
+ Args:
+ target_text (str): The text for which we want to estimate the duration.
+ ref_text (str): The reference text that was used to measure
+ the ref_duration.
+ ref_duration (float): The actual duration it took
+ to speak the ref_text.
+ low_threshold (float): The minimum duration threshold below which the
+ estimation will be considered unreliable.
+ boost_strength (float): Controls the power-curve boost for short durations.
+ Higher values boost small durations more aggressively.
+ 1 = no boost (linear), 2 = sqrt-like
+
+ Returns:
+ float: The estimated duration for the target_text based
+ on the ref_text and ref_duration.
+ """
+ if ref_duration <= 0 or not ref_text:
+ return 0.0
+
+ ref_weight = self.calculate_total_weight(ref_text)
+ if ref_weight == 0:
+ return 0.0
+
+ speed_factor = ref_weight / ref_duration
+ target_weight = self.calculate_total_weight(target_text)
+
+ estimated_duration = target_weight / speed_factor
+ if low_threshold is not None and estimated_duration < low_threshold:
+ alpha = 1.0 / boost_strength
+ return low_threshold * (estimated_duration / low_threshold) ** alpha
+ else:
+ return estimated_duration
+
+
+# ==========================================
+# Example Usage
+# ==========================================
+if __name__ == "__main__":
+ estimator = RuleDurationEstimator()
+
+ ref_txt = "Hello, world."
+ ref_dur = 1.5
+
+ test_cases = [
+ ("Hindi (With complex marks)", "नमस्ते दुनिया"),
+ ("Arabic (With vowels)", "مَرْحَبًا بِالْعَالَم"),
+ ("Vietnamese (Lots of diacritics)", "Chào thế giới"),
+ ("Chinese", "你好,世界!"),
+ ("Mixed Emoji", "Hello 🌍! This is fun 🎉"),
+ ]
+
+ print("--- Reference ---")
+ print(f"Reference Text: '{ref_txt}'")
+ print(f"Reference Duration: {ref_dur}s")
+ print("-" * 30)
+
+ for lang, txt in test_cases:
+ est_time = estimator.estimate_duration(txt, ref_txt, ref_dur)
+ weight = estimator.calculate_total_weight(txt)
+
+ print(f"[{lang}]")
+ print(f"Text: {txt}")
+ print(f"Total Weight: {weight:.2f}")
+ print(f"Estimated Duration: {est_time:.2f} s")
+ print("-" * 30)
diff --git a/omnivoice/utils/lang_map.py b/omnivoice/utils/lang_map.py
new file mode 100644
index 00000000..ffcda10a
--- /dev/null
+++ b/omnivoice/utils/lang_map.py
@@ -0,0 +1,698 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Language name to ISO 639-3 code mapping.
+
+Auto-generated from ``docs/lang_id_name_map.tsv``. Provides ``LANG_NAME_TO_ID``
+(for resolving language names to codes) and ``LANG_IDS`` (the set of supported
+ISO 639-3 codes). Used by ``OmniVoice.generate()`` to resolve user-provided
+language names.
+"""
+
+# Auto-generated from docs/lang_id_name_map.tsv
+# Maps lowercase language name -> language ID code
+
+LANG_NAME_TO_ID = {
+ "abadi": "kbt",
+ "abkhazian": "ab",
+ "abron": "abr",
+ "abua": "abn",
+ "adamawa fulfulde": "fub",
+ "adyghe": "ady",
+ "afade": "aal",
+ "afrikaans": "af",
+ "agwagwune": "yay",
+ "aja (benin)": "ajg",
+ "akebu": "keu",
+ "alago": "ala",
+ "albanian": "sq",
+ "algerian arabic": "arq",
+ "algerian saharan arabic": "aao",
+ "ambo-pasco quechua": "qva",
+ "ambonese malay": "abs",
+ "amdo tibetan": "adx",
+ "amharic": "am",
+ "anaang": "anw",
+ "angika": "anp",
+ "antankarana malagasy": "xmv",
+ "aragonese": "an",
+ "arbëreshë albanian": "aae",
+ "arequipa-la unión quechua": "qxu",
+ "armenian": "hy",
+ "ashe": "ahs",
+ "ashéninka perené": "prq",
+ "askopan": "eiv",
+ "assamese": "as",
+ "asturian": "ast",
+ "atayal": "tay",
+ "awak": "awo",
+ "ayacucho quechua": "quy",
+ "azerbaijani": "az",
+ "baatonum": "bba",
+ "bacama": "bcy",
+ "bade": "bde",
+ "bafia": "ksf",
+ "bafut": "bfd",
+ "bagirmi fulfulde": "fui",
+ "bago-kusuntu": "bqg",
+ "baharna arabic": "abv",
+ "bakoko": "bkh",
+ "balanta-ganja": "bjt",
+ "balti": "bft",
+ "bamenyam": "bce",
+ "bamun": "bax",
+ "bangwinji": "bsj",
+ "banjar": "bjn",
+ "bankon": "abb",
+ "baoulé": "bci",
+ "bara malagasy": "bhr",
+ "barok": "bjk",
+ "basa (cameroon)": "bas",
+ "basa (nigeria)": "bzw",
+ "bashkir": "ba",
+ "basque": "eu",
+ "batak mandailing": "btm",
+ "batanga": "bnm",
+ "bateri": "btv",
+ "bats": "bbl",
+ "bayot": "bda",
+ "bebele": "beb",
+ "belarusian": "be",
+ "bengali": "bn",
+ "betawi": "bew",
+ "bhili": "bhb",
+ "bhojpuri": "bho",
+ "bilur": "bxf",
+ "bima": "bhp",
+ "bodo": "brx",
+ "boghom": "bux",
+ "bokyi": "bky",
+ "bomu": "bmq",
+ "bondei": "bou",
+ "borgu fulfulde": "fue",
+ "bosnian": "bs",
+ "brahui": "brh",
+ "braj": "bra",
+ "breton": "br",
+ "buduma": "bdm",
+ "buginese": "bug",
+ "bukharic": "bhh",
+ "bulgarian": "bg",
+ "bulu (cameroon)": "bum",
+ "bundeli": "bns",
+ "bunun": "bnn",
+ "bura-pabir": "bwr",
+ "burak": "bys",
+ "burmese": "my",
+ "burushaski": "bsk",
+ "cacaloxtepec mixtec": "miu",
+ "cajatambo north lima quechua": "qvl",
+ "cakfem-mushere": "cky",
+ "cameroon pidgin": "wes",
+ "campidanese sardinian": "sro",
+ "cantonese": "yue",
+ "catalan": "ca",
+ "cebuano": "ceb",
+ "cen": "cen",
+ "central kurdish": "ckb",
+ "central nahuatl": "nhn",
+ "central pame": "pbs",
+ "central pashto": "pst",
+ "central puebla nahuatl": "ncx",
+ "central tarahumara": "tar",
+ "central yupik": "esu",
+ "central-eastern niger fulfulde": "fuq",
+ "chadian arabic": "shu",
+ "chichewa": "ny",
+ "chichicapan zapotec": "zpv",
+ "chiga": "cgg",
+ "chimalapa zoque": "zoh",
+ "chimborazo highland quichua": "qug",
+ "chinese": "zh",
+ "chiquián ancash quechua": "qxa",
+ "chitwania tharu": "the",
+ "chokwe": "cjk",
+ "chuvash": "cv",
+ "cibak": "ckl",
+ "coastal konjo": "kjc",
+ "copainalá zoque": "zoc",
+ "cornish": "kw",
+ "corongo ancash quechua": "qwa",
+ "croatian": "hr",
+ "cross river mbembe": "mfn",
+ "cuyamecalco mixtec": "xtu",
+ "czech": "cs",
+ "dadiya": "dbd",
+ "dagbani": "dag",
+ "dameli": "dml",
+ "danish": "da",
+ "dargwa": "dar",
+ "dazaga": "dzg",
+ "deccan": "dcc",
+ "degema": "deg",
+ "dera (nigeria)": "kna",
+ "dghwede": "dgh",
+ "dhatki": "mki",
+ "dhivehi": "dv",
+ "dhofari arabic": "adf",
+ "dijim-bwilim": "cfa",
+ "dogri": "dgo",
+ "domaaki": "dmk",
+ "dotyali": "dty",
+ "duala": "dua",
+ "dutch": "nl",
+ "dũya": "ldb",
+ "dyula": "dyu",
+ "eastern balochi": "bgp",
+ "eastern bolivian guaraní": "gui",
+ "eastern egyptian bedawi arabic": "avl",
+ "eastern krahn": "kqo",
+ "eastern mari": "mhr",
+ "eastern yiddish": "ydd",
+ "ebrié": "ebr",
+ "eggon": "ego",
+ "egyptian arabic": "arz",
+ "ejagham": "etu",
+ "eleme": "elm",
+ "eloyi": "afo",
+ "embu": "ebu",
+ "english": "en",
+ "erzya": "myv",
+ "esan": "ish",
+ "esperanto": "eo",
+ "estonian": "et",
+ "eton (cameroon)": "eto",
+ "ewondo": "ewo",
+ "extremaduran": "ext",
+ "fang (equatorial guinea)": "fan",
+ "fanti": "fat",
+ "farefare": "gur",
+ "fe'fe'": "fmp",
+ "filipino": "fil",
+ "filomena mata-coahuitlán totonac": "tlp",
+ "finnish": "fi",
+ "fipa": "fip",
+ "french": "fr",
+ "fulah": "ff",
+ "galician": "gl",
+ "gambian wolof": "wof",
+ "ganda": "lg",
+ "garhwali": "gbm",
+ "gawar-bati": "gwt",
+ "gawri": "gwc",
+ "gbagyi": "gbr",
+ "gbari": "gby",
+ "geji": "gyz",
+ "gen": "gej",
+ "georgian": "ka",
+ "german": "de",
+ "geser-gorom": "ges",
+ "gheg albanian": "aln",
+ "ghomálá'": "bbj",
+ "gidar": "gid",
+ "glavda": "glw",
+ "goan konkani": "gom",
+ "goaria": "gig",
+ "goemai": "ank",
+ "gola": "gol",
+ "greek": "el",
+ "guarani": "gn",
+ "guduf-gava": "gdf",
+ "guerrero amuzgo": "amu",
+ "gujarati": "gu",
+ "gujari": "gju",
+ "gulf arabic": "afb",
+ "gurgula": "ggg",
+ "gusii": "guz",
+ "gusilay": "gsl",
+ "gweno": "gwe",
+ "güilá zapotec": "ztu",
+ "hadothi": "hoj",
+ "hahon": "hah",
+ "haitian": "ht",
+ "hakha chin": "cnh",
+ "hakö": "hao",
+ "halia": "hla",
+ "hausa": "ha",
+ "hawaiian": "haw",
+ "hazaragi": "haz",
+ "hebrew": "he",
+ "hemba": "hem",
+ "herero": "hz",
+ "highland konjo": "kjk",
+ "hijazi arabic": "acw",
+ "hindi": "hi",
+ "huarijio": "var",
+ "huautla mazatec": "mau",
+ "huaxcaleca nahuatl": "nhq",
+ "huba": "hbb",
+ "huitepec mixtec": "mxs",
+ "hula": "hul",
+ "hungarian": "hu",
+ "hunjara-kaina ke": "hkk",
+ "hwana": "hwo",
+ "ibibio": "ibb",
+ "icelandic": "is",
+ "idakho-isukha-tiriki": "ida",
+ "idoma": "idu",
+ "igbo": "ig",
+ "igo": "ahl",
+ "ikposo": "kpo",
+ "ikwere": "ikw",
+ "imbabura highland quichua": "qvi",
+ "indonesian": "id",
+ "indus kohistani": "mvy",
+ "interlingua (international auxiliary language association)": "ia",
+ "inupiaq": "ik",
+ "irish": "ga",
+ "iron ossetic": "os",
+ "isekiri": "its",
+ "isoko": "iso",
+ "italian": "it",
+ "ito": "itw",
+ "itzá": "itz",
+ "ixtayutla mixtec": "vmj",
+ "izon": "ijc",
+ "jambi malay": "jax",
+ "japanese": "ja",
+ "jaqaru": "jqr",
+ "jauja wanca quechua": "qxw",
+ "jaunsari": "jns",
+ "javanese": "jv",
+ "jiba": "juo",
+ "jju": "kaj",
+ "judeo-moroccan arabic": "aju",
+ "juxtlahuaca mixtec": "vmc",
+ "kabardian": "kbd",
+ "kabras": "lkb",
+ "kabuverdianu": "kea",
+ "kabyle": "kab",
+ "kachi koli": "gjk",
+ "kairak": "ckr",
+ "kalabari": "ijn",
+ "kalasha": "kls",
+ "kalenjin": "kln",
+ "kalkoti": "xka",
+ "kamba": "kam",
+ "kamo": "kcq",
+ "kanauji": "bjj",
+ "kanembu": "kbl",
+ "kannada": "kn",
+ "karekare": "kai",
+ "kashmiri": "ks",
+ "kathoriya tharu": "tkt",
+ "kati": "bsh",
+ "kazakh": "kk",
+ "keiyo": "eyo",
+ "khams tibetan": "khg",
+ "khana": "ogo",
+ "khetrani": "xhe",
+ "khmer": "km",
+ "khowar": "khw",
+ "kinga": "zga",
+ "kinnauri": "kfk",
+ "kinyarwanda": "rw",
+ "kirghiz": "ky",
+ "kirya-konzəl": "fkk",
+ "kochila tharu": "thq",
+ "kohistani shina": "plk",
+ "kohumono": "bcs",
+ "kok borok": "trp",
+ "kol (papua new guinea)": "kol",
+ "kom (cameroon)": "bkm",
+ "koma": "kmy",
+ "konkani": "knn",
+ "konzo": "koo",
+ "korean": "ko",
+ "korwa": "kfp",
+ "kota (india)": "kfe",
+ "koti": "eko",
+ "kuanua": "ksd",
+ "kuanyama": "kj",
+ "kui (india)": "uki",
+ "kulung (nigeria)": "bbu",
+ "kuot": "kto",
+ "kushi": "kuh",
+ "kwambi": "kwm",
+ "kwasio": "nmg",
+ "lala-roba": "lla",
+ "lamang": "hia",
+ "lao": "lo",
+ "larike-wakasihu": "alo",
+ "lasi": "lss",
+ "latgalian": "ltg",
+ "latvian": "lv",
+ "levantine arabic": "apc",
+ "liana-seti": "ste",
+ "liberia kpelle": "xpe",
+ "liberian english": "lir",
+ "libyan arabic": "ayl",
+ "ligurian": "lij",
+ "lijili": "mgi",
+ "lingala": "ln",
+ "lithuanian": "lt",
+ "loarki": "lrk",
+ "logooli": "rag",
+ "logudorese sardinian": "src",
+ "loja highland quichua": "qvj",
+ "loloda": "loa",
+ "longuda": "lnu",
+ "loxicha zapotec": "ztp",
+ "luba-lulua": "lua",
+ "luo": "luo",
+ "lushai": "lus",
+ "luxembourgish": "lb",
+ "maasina fulfulde": "ffm",
+ "maba (chad)": "mde",
+ "macedo-romanian": "rup",
+ "macedonian": "mk",
+ "mada (cameroon)": "mxu",
+ "mafa": "maf",
+ "maithili": "mai",
+ "malay": "ms",
+ "malayalam": "ml",
+ "mali": "gcc",
+ "malinaltepec me'phaa": "tcf",
+ "maltese": "mt",
+ "mandara": "tbf",
+ "mandjak": "mfv",
+ "manggarai": "mqy",
+ "manipuri": "mni",
+ "mansoanka": "msw",
+ "manx": "gv",
+ "maori": "mi",
+ "marathi": "mr",
+ "marghi central": "mrt",
+ "marghi south": "mfm",
+ "maria (india)": "mrr",
+ "marwari (pakistan)": "mve",
+ "masana": "mcn",
+ "masikoro malagasy": "msh",
+ "matsés": "mcf",
+ "mazaltepec zapotec": "zpy",
+ "mazatlán mazatec": "vmz",
+ "mazatlán mixe": "mzl",
+ "mbe": "mfo",
+ "mbo (cameroon)": "mbo",
+ "mbum": "mdd",
+ "medumba": "byv",
+ "mekeo": "mek",
+ "meru": "mer",
+ "mesopotamian arabic": "acm",
+ "mewari": "mtr",
+ "min nan chinese": "nan",
+ "mingrelian": "xmf",
+ "mitlatongo mixtec": "vmm",
+ "miya": "mkf",
+ "mokpwe": "bri",
+ "moksha": "mdf",
+ "mom jango": "ver",
+ "mongolian": "mn",
+ "moroccan arabic": "ary",
+ "motu": "meu",
+ "mpiemo": "mcx",
+ "mpumpong": "mgg",
+ "mundang": "mua",
+ "mungaka": "mhk",
+ "musey": "mse",
+ "musgu": "mug",
+ "musi": "mui",
+ "naba": "mne",
+ "najdi arabic": "ars",
+ "nalik": "nal",
+ "nawdm": "nmz",
+ "ndonga": "ng",
+ "neapolitan": "nap",
+ "nepali": "npi",
+ "ngamo": "nbh",
+ "ngas": "anc",
+ "ngiemboon": "nnh",
+ "ngizim": "ngi",
+ "ngomba": "jgo",
+ "ngombale": "nla",
+ "nigerian fulfulde": "fuv",
+ "nigerian pidgin": "pcm",
+ "nimadi": "noe",
+ "nobiin": "fia",
+ "north mesopotamian arabic": "ayp",
+ "north moluccan malay": "max",
+ "northern betsimisaraka malagasy": "bmm",
+ "northern hindko": "hno",
+ "northern kurdish": "kmr",
+ "northern pame": "pmq",
+ "northern pashto": "pbu",
+ "northern uzbek": "uzn",
+ "northwest gbaya": "gya",
+ "norwegian": "no",
+ "norwegian bokmål": "nb",
+ "norwegian nynorsk": "nn",
+ "notsi": "ncf",
+ "nyankpa": "yes",
+ "nyungwe": "nyu",
+ "nzanyi": "nja",
+ "nüpode huitoto": "hux",
+ "occitan": "oc",
+ "od": "odk",
+ "odia": "ory",
+ "odual": "odu",
+ "omani arabic": "acx",
+ "orizaba nahuatl": "nlv",
+ "orma": "orc",
+ "ormuri": "oru",
+ "oromo": "om",
+ "pahari-potwari": "phr",
+ "paiwan": "pwn",
+ "panjabi": "pa",
+ "papuan malay": "pmy",
+ "parkari koli": "kvx",
+ "pedi": "nso",
+ "pero": "pip",
+ "persian": "fa",
+ "petats": "pex",
+ "phalura": "phl",
+ "piemontese": "pms",
+ "piya-kwonci": "piy",
+ "plateau malagasy": "plt",
+ "polish": "pl",
+ "poqomam": "poc",
+ "portuguese": "pt",
+ "pulaar": "fuc",
+ "pular": "fuf",
+ "puno quechua": "qxp",
+ "pushto": "ps",
+ "pökoot": "pko",
+ "qaqet": "byx",
+ "quiotepec chinantec": "chq",
+ "rana tharu": "thr",
+ "rangi": "lag",
+ "rapoisi": "kyx",
+ "ratahan": "rth",
+ "rayón zoque": "zor",
+ "romanian": "ro",
+ "romansh": "rm",
+ "rombo": "rof",
+ "rotokas": "roo",
+ "rukai": "dru",
+ "russian": "ru",
+ "sacapulteco": "quv",
+ "saidi arabic": "aec",
+ "sakalava malagasy": "skg",
+ "sakizaya": "szy",
+ "saleman": "sau",
+ "samba daka": "ccg",
+ "samba leko": "ndi",
+ "san felipe otlaltepec popoloca": "pow",
+ "san francisco del mar huave": "hue",
+ "san juan atzingo popoloca": "poe",
+ "san martín itunyoso triqui": "trq",
+ "san miguel el grande mixtec": "mig",
+ "sansi": "ssi",
+ "sanskrit": "sa",
+ "santa ana de tusi pasco quechua": "qxt",
+ "santa catarina albarradas zapotec": "ztn",
+ "santali": "sat",
+ "santiago del estero quichua": "qus",
+ "saposa": "sps",
+ "saraiki": "skr",
+ "sardinian": "sc",
+ "saya": "say",
+ "sediq": "trv",
+ "serbian": "sr",
+ "seri": "sei",
+ "shina": "scl",
+ "shona": "sn",
+ "siar-lak": "sjr",
+ "sibe": "nco",
+ "sicilian": "scn",
+ "sihuas ancash quechua": "qws",
+ "sikkimese": "sip",
+ "sinaugoro": "snc",
+ "sindhi": "sd",
+ "sindhi bhil": "sbn",
+ "sinhala": "si",
+ "sinicahua mixtec": "xti",
+ "sipacapense": "qum",
+ "siwai": "siw",
+ "slovak": "sk",
+ "slovenian": "sl",
+ "solos": "sol",
+ "somali": "so",
+ "soninke": "snk",
+ "south giziga": "giz",
+ "south ucayali ashéninka": "cpy",
+ "southeastern nochixtlán mixtec": "mxy",
+ "southern betsimisaraka malagasy": "bzc",
+ "southern pashto": "pbt",
+ "southern pastaza quechua": "qup",
+ "soyaltepec mazatec": "vmp",
+ "spanish": "es",
+ "standard arabic": "arb",
+ "standard moroccan tamazight": "zgh",
+ "sudanese arabic": "apd",
+ "sulka": "sua",
+ "svan": "sva",
+ "swahili": "sw",
+ "swedish": "sv",
+ "tae'": "rob",
+ "tahaggart tamahaq": "thv",
+ "taita": "dav",
+ "tajik": "tg",
+ "tamil": "ta",
+ "tandroy-mahafaly malagasy": "tdx",
+ "tangale": "tan",
+ "tanosy malagasy": "txy",
+ "tarok": "yer",
+ "tatar": "tt",
+ "tedaga": "tuq",
+ "telugu": "te",
+ "tem": "kdh",
+ "teop": "tio",
+ "tepeuxila cuicatec": "cux",
+ "tepinapa chinantec": "cte",
+ "tera": "ttr",
+ "terei": "buo",
+ "termanu": "twu",
+ "tesaka malagasy": "tkg",
+ "tetelcingo nahuatl": "nhg",
+ "teutila cuicatec": "cut",
+ "thai": "th",
+ "tibetan": "bo",
+ "tidaá mixtec": "mtx",
+ "tidore": "tvo",
+ "tigak": "tgc",
+ "tigre": "tig",
+ "tigrinya": "ti",
+ "tilquiapan zapotec": "zts",
+ "tinputz": "tpz",
+ "tlacoapa me'phaa": "tpl",
+ "tlacoatzintepec chinantec": "ctl",
+ "tlingit": "tli",
+ "toki pona": "tok",
+ "tomoip": "tqp",
+ "tondano": "tdn",
+ "tonsea": "txs",
+ "tooro": "ttj",
+ "torau": "ttu",
+ "torwali": "trw",
+ "tsimihety malagasy": "xmw",
+ "tsotso": "lto",
+ "tswana": "tn",
+ "tugen": "tuy",
+ "tuki": "bag",
+ "tula": "tul",
+ "tulu": "tcy",
+ "tunen": "tvu",
+ "tungag": "lcm",
+ "tunisian arabic": "aeb",
+ "tupuri": "tui",
+ "turkana": "tuv",
+ "turkish": "tr",
+ "turkmen": "tk",
+ "tututepec mixtec": "mtu",
+ "twi": "tw",
+ "ubaghara": "byc",
+ "uighur": "ug",
+ "ukrainian": "uk",
+ "umbundu": "umb",
+ "upper sorbian": "hsb",
+ "urdu": "ur",
+ "ushojo": "ush",
+ "uzbek": "uz",
+ "vai": "vai",
+ "vietnamese": "vi",
+ "votic": "vot",
+ "võro": "vro",
+ "waci gbe": "wci",
+ "wadiyara koli": "kxp",
+ "waja": "wja",
+ "wakhi": "wbl",
+ "wanga": "lwg",
+ "wapan": "juk",
+ "warji": "wji",
+ "welsh": "cy",
+ "wemale": "weo",
+ "western frisian": "fy",
+ "western highland purepecha": "pua",
+ "western juxtlahuaca mixtec": "jmx",
+ "western maninkakan": "mlq",
+ "western mari": "mrj",
+ "western niger fulfulde": "fuh",
+ "western panjabi": "pnb",
+ "wolof": "wo",
+ "wuzlam": "udl",
+ "xanaguía zapotec": "ztg",
+ "xhosa": "xh",
+ "yace": "ekr",
+ "yakut": "sah",
+ "yalahatan": "jal",
+ "yanahuanca pasco quechua": "qur",
+ "yangben": "yav",
+ "yaqui": "yaq",
+ "yauyos quechua": "qux",
+ "yekhee": "ets",
+ "yiddish": "yi",
+ "yidgha": "ydg",
+ "yoruba": "yo",
+ "yutanduchi mixtec": "mab",
+ "zacatlán-ahuacatlán-tepetzintla nahuatl": "nhi",
+ "zarma": "dje",
+ "zaza": "zza",
+ "zulu": "zu",
+ "ömie": "aom",
+}
+
+LANG_NAMES = set(LANG_NAME_TO_ID.keys())
+LANG_IDS = set(LANG_NAME_TO_ID.values())
+
+# Exceptions where .title() doesn't match the canonical casing from the TSV.
+_TITLE_EXCEPTIONS = {
+ "fe'fe'": "Fe'fe'",
+ "dũya": "Dũya",
+ "santiago del estero quichua": "Santiago del Estero Quichua",
+ "santa ana de tusi pasco quechua": "Santa Ana de Tusi Pasco Quechua",
+ "malinaltepec me'phaa": "Malinaltepec Me'phaa",
+ "tlacoapa me'phaa": "Tlacoapa Me'phaa",
+}
+
+
+def lang_display_name(name: str) -> str:
+ """Return a display-friendly version of a lowercase language name.
+
+ Uses .title() for most names, with manual exceptions for cases like
+ apostrophes and small words (de, del) that should stay lowercase.
+ """
+ return _TITLE_EXCEPTIONS.get(name, name.title())
diff --git a/omnivoice/utils/text.py b/omnivoice/utils/text.py
new file mode 100644
index 00000000..3fc9adb0
--- /dev/null
+++ b/omnivoice/utils/text.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Text processing utilities for TTS inference.
+
+Provides:
+- ``chunk_text_punctuation()``: Splits long text into model-friendly chunks at
+ sentence boundaries, with abbreviation-aware punctuation splitting.
+- ``add_punctuation()``: Appends missing end punctuation (Chinese or English).
+"""
+
+from typing import List, Optional
+
+
+SPLIT_PUNCTUATION = set(".,;:!?。,;:!?")
+CLOSING_MARKS = set("\"'""')]》》>」】")
+
+END_PUNCTUATION = {
+ ";",
+ ":",
+ ",",
+ ".",
+ "!",
+ "?",
+ "…",
+ ")",
+ "]",
+ "}",
+ '"',
+ "'",
+ """,
+ "'",
+ ";",
+ ":",
+ ",",
+ "。",
+ "!",
+ "?",
+ "、",
+ "……",
+ ")",
+ "】",
+ """,
+ "'",
+}
+
+
+ABBREVIATIONS = {
+ "Mr.",
+ "Mrs.",
+ "Ms.",
+ "Dr.",
+ "Prof.",
+ "Sr.",
+ "Jr.",
+ "Rev.",
+ "Fr.",
+ "Hon.",
+ "Pres.",
+ "Gov.",
+ "Capt.",
+ "Gen.",
+ "Sen.",
+ "Rep.",
+ "Col.",
+ "Maj.",
+ "Lt.",
+ "Cmdr.",
+ "Sgt.",
+ "Cpl.",
+ "Co.",
+ "Corp.",
+ "Inc.",
+ "Ltd.",
+ "Est.",
+ "Dept.",
+ "St.",
+ "Ave.",
+ "Blvd.",
+ "Rd.",
+ "Mt.",
+ "Ft.",
+ "No.",
+ "Jan.",
+ "Feb.",
+ "Mar.",
+ "Apr.",
+ "Aug.",
+ "Sep.",
+ "Sept.",
+ "Oct.",
+ "Nov.",
+ "Dec.",
+ "i.e.",
+ "e.g.",
+ "vs.",
+ "Vs.",
+ "Etc.",
+ "approx.",
+ "fig.",
+ "def.",
+}
+
+
+def chunk_text_punctuation(
+ text: str,
+ chunk_len: int,
+ min_chunk_len: Optional[int] = None,
+) -> List[str]:
+ """
+ Splits the input tokens list into chunks according to punctuations,
+ avoiding splits on common abbreviations (e.g., Mr., No.).
+ """
+
+ # 1. Split the tokens according to punctuations.
+ sentences = []
+ current_sentence = []
+
+ tokens_list = list(text)
+
+ for token in tokens_list:
+ # If the first token of current sentence is punctuation,
+ # append it to the end of the previous sentence.
+ if (
+ len(current_sentence) == 0
+ and len(sentences) != 0
+ and (token in SPLIT_PUNCTUATION or token in CLOSING_MARKS)
+ ):
+ sentences[-1].append(token)
+ # Otherwise, append the current token to the current sentence.
+ else:
+ current_sentence.append(token)
+
+ # Split the sentence in positions of punctuations.
+ if token in SPLIT_PUNCTUATION:
+ is_abbreviation = False
+
+ if token == ".":
+ temp_str = "".join(current_sentence).strip()
+ if temp_str:
+ last_word = temp_str.split()[-1]
+ if last_word in ABBREVIATIONS:
+ is_abbreviation = True
+
+ if not is_abbreviation:
+ sentences.append(current_sentence)
+ current_sentence = []
+ # Assume the last few tokens are also a sentence
+ if len(current_sentence) != 0:
+ sentences.append(current_sentence)
+
+ # 2. Merge short sentences.
+ merged_chunks = []
+ current_chunk = []
+ for sentence in sentences:
+ if len(current_chunk) + len(sentence) <= chunk_len:
+ current_chunk.extend(sentence)
+ else:
+ if len(current_chunk) > 0:
+ merged_chunks.append(current_chunk)
+ current_chunk = sentence
+
+ if len(current_chunk) > 0:
+ merged_chunks.append(current_chunk)
+
+ # 4. Post-process: Check for undersized chunks and merge them
+ # with the previous chunk or next chunk (if it's the first chunk).
+ if min_chunk_len is not None:
+ first_chunk_short_flag = (
+ len(merged_chunks) > 0 and len(merged_chunks[0]) < min_chunk_len
+ )
+ final_chunks = []
+ for i, chunk in enumerate(merged_chunks):
+ if i == 1 and first_chunk_short_flag:
+ final_chunks[-1].extend(chunk)
+ else:
+ if len(chunk) >= min_chunk_len:
+ final_chunks.append(chunk)
+ else:
+ if len(final_chunks) == 0:
+ final_chunks.append(chunk)
+ else:
+ final_chunks[-1].extend(chunk)
+ else:
+ final_chunks = merged_chunks
+
+ chunk_strings = [
+ "".join(chunk).strip() for chunk in final_chunks if "".join(chunk).strip()
+ ]
+ return chunk_strings
+
+
+def add_punctuation(text: str):
+ """Add punctuation if there is not in the end of text"""
+ text = text.strip()
+
+ if not text:
+ return text
+
+ if text[-1] not in END_PUNCTUATION:
+ is_chinese = any("\u4e00" <= char <= "\u9fff" for char in text)
+
+ text += "。" if is_chinese else "."
+
+ return text
diff --git a/omnivoice/utils/voice_design.py b/omnivoice/utils/voice_design.py
new file mode 100644
index 00000000..802321d5
--- /dev/null
+++ b/omnivoice/utils/voice_design.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# Copyright 2026 Xiaomi Corp. (authors: Han Zhu)
+#
+# See ../../LICENSE for clarification regarding multiple authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Voice-design instruct constants for TTS inference.
+
+Defines speaker attribute tags (gender, age, pitch, accent, dialect) and
+translation/validation utilities between English and Chinese. Used by
+``OmniVoice.generate()`` for voice design mode.
+"""
+
+import re
+
+_ZH_RE = re.compile(r'[\u4e00-\u9fff]')
+
+# Category = set of {english: chinese, ...} items that are mutually exclusive.
+# Accent (EN-only) and dialect (ZH-only) are stored as flat sets below.
+_INSTRUCT_CATEGORIES = [
+ {"male": "男", "female": "女"},
+ {"child": "儿童", "teenager": "少年", "young adult": "青年",
+ "middle-aged": "中年", "elderly": "老年"},
+ {"very low pitch": "极低音调", "low pitch": "低音调",
+ "moderate pitch": "中音调", "high pitch": "高音调",
+ "very high pitch": "极高音调"},
+ {"whisper": "耳语"},
+ # Accent (English-only, no Chinese counterpart)
+ {"american accent", "british accent", "australian accent",
+ "chinese accent", "canadian accent", "indian accent",
+ "korean accent", "portuguese accent", "russian accent", "japanese accent"},
+ # Dialect (Chinese-only, no English counterpart)
+ {"河南话", "陕西话", "四川话", "贵州话", "云南话", "桂林话",
+ "济南话", "石家庄话", "甘肃话", "宁夏话", "青岛话", "东北话"},
+]
+
+_INSTRUCT_EN_TO_ZH = {}
+_INSTRUCT_ZH_TO_EN = {}
+_INSTRUCT_MUTUALLY_EXCLUSIVE = []
+for _cat in _INSTRUCT_CATEGORIES:
+ if isinstance(_cat, dict):
+ _INSTRUCT_EN_TO_ZH.update(_cat)
+ _INSTRUCT_ZH_TO_EN.update({v: k for k, v in _cat.items()})
+ _INSTRUCT_MUTUALLY_EXCLUSIVE.append(set(_cat) | set(_cat.values()))
+ else:
+ _INSTRUCT_MUTUALLY_EXCLUSIVE.append(set(_cat))
+
+_INSTRUCT_ALL_VALID = (
+ set(_INSTRUCT_EN_TO_ZH) | set(_INSTRUCT_ZH_TO_EN)
+ | _INSTRUCT_MUTUALLY_EXCLUSIVE[-2] # accents
+ | _INSTRUCT_MUTUALLY_EXCLUSIVE[-1] # dialects
+)
+
+_INSTRUCT_VALID_EN = frozenset(i for i in _INSTRUCT_ALL_VALID if not _ZH_RE.search(i))
+_INSTRUCT_VALID_ZH = frozenset(i for i in _INSTRUCT_ALL_VALID if _ZH_RE.search(i))
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..877f69b4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,106 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "omnivoice"
+version = "0.1.3"
+description = "OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models"
+readme = "README.md"
+license = "Apache-2.0"
+requires-python = ">=3.10"
+authors = [{name = "Han Zhu"}]
+keywords = [
+ "tts",
+ "text-to-speech",
+ "speech-synthesis",
+ "zero-shot",
+ "multilingual",
+ "diffusion",
+ "voice-cloning",
+]
+classifiers = [
+ "Intended Audience :: Science/Research",
+ "Intended Audience :: Developers",
+
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
+
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+]
+dependencies = [
+ "torch>=2.4",
+ "torchaudio>=2.4",
+ "transformers>=5.3.0",
+ "accelerate",
+ "pydub",
+ "gradio",
+ "tensorboardX",
+ "webdataset",
+ "numpy",
+ "soundfile"
+]
+
+[project.optional-dependencies]
+
+eval = [
+ "jiwer==3.1.0", # WER
+ "librosa", # Audio processing
+ "s3prl", # Speech representation (HuBERT etc.)
+ "funasr", # ASR models
+ "zhconv", # Chinese character normalization
+ "zhon", # Chinese punctuation
+ "unidecode", # Unicode normalization
+]
+api = [
+ "fastapi",
+ "uvicorn",
+ "python-multipart"
+]
+ui = [
+ "gradio",
+ "gradio_client",
+ "requests",
+]
+
+[project.scripts]
+omnivoice-infer = "omnivoice.cli.infer:main"
+omnivoice-infer-batch = "omnivoice.cli.infer_batch:main"
+omnivoice-demo = "omnivoice.cli.demo:main"
+
+[project.urls]
+Homepage = "https://github.com/k2-fsa/OmniVoice"
+Repository = "https://github.com/k2-fsa/OmniVoice"
+"Bug Tracker" = "https://github.com/k2-fsa/OmniVoice/issues"
+
+[tool.uv.sources]
+# Install PyTorch with CUDA support on Linux/Windows (CUDA doesn't exist for Mac).
+# NOTE: We must explicitly request them as `dependencies` above. These improved
+# versions will not be selected if they're only third-party dependencies.
+torch = [
+ { index = "pytorch-cuda", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+torchaudio = [
+ { index = "pytorch-cuda", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[[tool.uv.index]]
+name = "pytorch-cuda"
+# Use PyTorch built for NVIDIA Toolkit version 12.8.
+# Available versions: https://pytorch.org/get-started/locally/
+url = "https://download.pytorch.org/whl/cu128"
+# Only use this index when explicitly requested by `tool.uv.sources`.
+explicit = true
+
+[tool.uv]
+constraint-dependencies = [
+ "torch==2.8.0",
+ "torchaudio==2.8.0",
+]
+
+[tool.hatch.build.targets.sdist]
+include = ["omnivoice"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["omnivoice"]
diff --git a/ui.py b/ui.py
new file mode 100644
index 00000000..93d60937
--- /dev/null
+++ b/ui.py
@@ -0,0 +1,206 @@
+import gradio as gr
+from gradio_client import Client, handle_file
+import tempfile
+import os
+
+from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name
+
+_ALL_LANGUAGES = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES)
+
+_CATEGORIES = {
+ "Gender / 性别": ["Male / 男", "Female / 女"],
+ "Age / 年龄": [
+ "Child / 儿童",
+ "Teenager / 少年",
+ "Young Adult / 青年",
+ "Middle-aged / 中年",
+ "Elderly / 老年",
+ ],
+ "Pitch / 音调": [
+ "Very Low Pitch / 极低音调",
+ "Low Pitch / 低音调",
+ "Moderate Pitch / 中音调",
+ "High Pitch / 高音调",
+ "Very High Pitch / 极高音调",
+ ],
+ "Style / 风格": ["Whisper / 耳语"],
+ "English Accent / 英文口音": [
+ "American Accent / 美式口音",
+ "Australian Accent / 澳大利亚口音",
+ "British Accent / 英国口音",
+ "Chinese Accent / 中国口音",
+ "Canadian Accent / 加拿大口音",
+ "Indian Accent / 印度口音",
+ "Korean Accent / 韩国口音",
+ "Portuguese Accent / 葡萄牙口音",
+ "Russian Accent / 俄罗斯口音",
+ "Japanese Accent / 日本口音",
+ ],
+ "Chinese Dialect / 中文方言": [
+ "Henan Dialect / 河南话",
+ "Shaanxi Dialect / 陕西话",
+ "Sichuan Dialect / 四川话",
+ "Guizhou Dialect / 贵州话",
+ "Yunnan Dialect / 云南话",
+ "Guilin Dialect / 桂林话",
+ "Jinan Dialect / 济南话",
+ "Shijiazhuang Dialect / 石家庄话",
+ "Gansu Dialect / 甘肃话",
+ "Ningxia Dialect / 宁夏话",
+ "Qingdao Dialect / 青岛话",
+ "Northeast Dialect / 东北话",
+ ],
+}
+
+_ATTR_INFO = {
+ "English Accent / 英文口音": "Only effective for English speech.",
+ "Chinese Dialect / 中文方言": "Only effective for Chinese speech.",
+}
+
+client = Client("k2-fsa/OmniVoice")
+
+def _lang_dropdown(label="Language (optional) / 语种 (可选)", value="Auto"):
+ return gr.Dropdown(
+ label=label,
+ choices=_ALL_LANGUAGES,
+ value=value,
+ allow_custom_value=False,
+ interactive=True,
+ )
+
+def _gen_settings():
+ with gr.Accordion("Generation Settings (optional)", open=False):
+ sp = gr.Slider(0.5, 1.5, value=1.0, step=0.05, label="Speed")
+ du = gr.Number(value=0, label="Duration (seconds)") # Used 0 instead of None to match number input requirements, handle inside logic if needed
+ ns = gr.Slider(4, 64, value=32, step=1, label="Inference Steps")
+ dn = gr.Checkbox(label="Denoise", value=True)
+ gs = gr.Slider(0.0, 4.0, value=2.0, step=0.1, label="Guidance Scale (CFG)")
+ pp = gr.Checkbox(label="Preprocess Prompt", value=True)
+ po = gr.Checkbox(label="Postprocess Output", value=True)
+ return ns, gs, dn, sp, du, pp, po
+
+theme = gr.themes.Soft(font=["Inter", "Arial", "sans-serif"])
+css = """
+.gradio-container {max-width: 100% !important; font-size: 16px !important;}
+.gradio-container h1 {font-size: 1.5em !important;}
+.gradio-container .prose {font-size: 1.1em !important;}
+.compact-audio audio {height: 60px !important;}
+.compact-audio .waveform {min-height: 80px !important;}
+"""
+
+with gr.Blocks(theme=theme, css=css, title="OmniVoice Client UI") as demo:
+ gr.Markdown(
+ """
+ # OmniVoice Client UI
+
+ This UI connects remotely to the `k2-fsa/OmniVoice` space via API. It supports Voice Clone and Voice Design.
+ """
+ )
+
+ with gr.Tabs():
+ # ================= Voice Clone Tab =================
+ with gr.TabItem("Voice Clone"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vc_text = gr.Textbox(label="Text to Synthesize / 待合成文本", lines=4)
+ vc_ref_audio = gr.Audio(label="Reference Audio / 参考音频", type="filepath", elem_classes="compact-audio")
+ vc_ref_text = gr.Textbox(label="Reference Text (optional) / 参考音频文本(可选)", lines=2)
+ vc_lang = _lang_dropdown()
+
+ with gr.Accordion("Instruct (optional)", open=False):
+ vc_instruct = gr.Textbox(label="Instruct", lines=2)
+
+ vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po = _gen_settings()
+ vc_btn = gr.Button("Generate / 生成", variant="primary")
+
+ with gr.Column(scale=1):
+ vc_audio = gr.Audio(label="Output Audio / 合成结果", type="filepath")
+ vc_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def clone_wrapper(text, lang, ref_aud, ref_text, instruct, ns, gs, dn, sp, du, pp, po):
+ if not ref_aud:
+ return None, "Error: Must provide reference audio for Voice Clone."
+ try:
+ result = client.predict(
+ text=text or "",
+ lang=lang or "Auto",
+ ref_aud=handle_file(ref_aud),
+ ref_text=ref_text or "",
+ instruct=instruct or "",
+ ns=ns,
+ gs=gs,
+ dn=dn,
+ sp=sp,
+ du=du or 0,
+ pp=pp,
+ po=po,
+ api_name="/_clone_fn"
+ )
+ return result[0], result[1]
+ except Exception as e:
+ return None, f"Error: {e}"
+
+ vc_btn.click(
+ clone_wrapper,
+ inputs=[vc_text, vc_lang, vc_ref_audio, vc_ref_text, vc_instruct, vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po],
+ outputs=[vc_audio, vc_status]
+ )
+
+ # ================= Voice Design Tab =================
+ with gr.TabItem("Voice Design"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vd_text = gr.Textbox(label="Text to Synthesize / 待合成文本", lines=4)
+ vd_lang = _lang_dropdown()
+
+ vd_groups = []
+ for _cat, _choices in _CATEGORIES.items():
+ vd_groups.append(
+ gr.Dropdown(
+ label=_cat,
+ choices=["Auto"] + _choices,
+ value="Auto",
+ info=_ATTR_INFO.get(_cat),
+ )
+ )
+
+ vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po = _gen_settings()
+ vd_btn = gr.Button("Generate / 生成", variant="primary")
+
+ with gr.Column(scale=1):
+ vd_audio = gr.Audio(label="Output Audio / 合成结果", type="filepath")
+ vd_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def design_wrapper(text, lang, ns, gs, dn, sp, du, pp, po, *groups):
+ try:
+ # groups mapped to param_9 ... param_14
+ result = client.predict(
+ text=text or "",
+ lang=lang or "Auto",
+ ns=ns,
+ gs=gs,
+ dn=dn,
+ sp=sp,
+ du=du or 0,
+ pp=pp,
+ po=po,
+ param_9=groups[0],
+ param_10=groups[1],
+ param_11=groups[2],
+ param_12=groups[3],
+ param_13=groups[4],
+ param_14=groups[5],
+ api_name="/_design_fn"
+ )
+ return result[0], result[1]
+ except Exception as e:
+ return None, f"Error: {e}"
+
+ vd_btn.click(
+ design_wrapper,
+ inputs=[vd_text, vd_lang, vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po] + vd_groups,
+ outputs=[vd_audio, vd_status]
+ )
+
+if __name__ == "__main__":
+ demo.launch(server_name="0.0.0.0", server_port=7860)
diff --git a/ui_local.py b/ui_local.py
new file mode 100644
index 00000000..e73acc59
--- /dev/null
+++ b/ui_local.py
@@ -0,0 +1,250 @@
+import gradio as gr
+import requests
+import tempfile
+import os
+
+from omnivoice.utils.lang_map import LANG_NAMES, lang_display_name
+
+API_URL = "http://127.0.0.1:8000/generate"
+
+_ALL_LANGUAGES = ["Auto"] + sorted(lang_display_name(n) for n in LANG_NAMES)
+
+_CATEGORIES = {
+ "Gender / 性别": ["Male / 男", "Female / 女"],
+ "Age / 年龄": [
+ "Child / 儿童",
+ "Teenager / 少年",
+ "Young Adult / 青年",
+ "Middle-aged / 中年",
+ "Elderly / 老年",
+ ],
+ "Pitch / 音调": [
+ "Very Low Pitch / 极低音调",
+ "Low Pitch / 低音调",
+ "Moderate Pitch / 中音调",
+ "High Pitch / 高音调",
+ "Very High Pitch / 极高音调",
+ ],
+ "Style / 风格": ["Whisper / 耳语"],
+ "English Accent / 英文口音": [
+ "American Accent / 美式口音",
+ "Australian Accent / 澳大利亚口音",
+ "British Accent / 英国口音",
+ "Chinese Accent / 中国口音",
+ "Canadian Accent / 加拿大口音",
+ "Indian Accent / 印度口音",
+ "Korean Accent / 韩国口音",
+ "Portuguese Accent / 葡萄牙口音",
+ "Russian Accent / 俄罗斯口音",
+ "Japanese Accent / 日本口音",
+ ],
+ "Chinese Dialect / 中文方言": [
+ "Henan Dialect / 河南话",
+ "Shaanxi Dialect / 陕西话",
+ "Sichuan Dialect / 四川话",
+ "Guizhou Dialect / 贵州话",
+ "Yunnan Dialect / 云南话",
+ "Guilin Dialect / 桂林话",
+ "Jinan Dialect / 济南话",
+ "Shijiazhuang Dialect / 石家庄话",
+ "Gansu Dialect / 甘肃话",
+ "Ningxia Dialect / 宁夏话",
+ "Qingdao Dialect / 青岛话",
+ "Northeast Dialect / 东北话",
+ ],
+}
+
+_ATTR_INFO = {
+ "English Accent / 英文口音": "Only effective for English speech.",
+ "Chinese Dialect / 中文方言": "Only effective for Chinese speech.",
+}
+
+def _lang_dropdown(label="Language (optional) / 语种 (可选)", value="Auto"):
+ return gr.Dropdown(
+ label=label,
+ choices=_ALL_LANGUAGES,
+ value=value,
+ allow_custom_value=False,
+ interactive=True,
+ )
+
+def _gen_settings():
+ with gr.Accordion("Generation Settings (optional)", open=False):
+ sp = gr.Slider(0.5, 1.5, value=1.0, step=0.05, label="Speed")
+ du = gr.Number(value=0, label="Duration (seconds)")
+ ns = gr.Slider(4, 64, value=32, step=1, label="Inference Steps")
+ dn = gr.Checkbox(label="Denoise", value=True)
+ gs = gr.Slider(0.0, 4.0, value=2.0, step=0.1, label="Guidance Scale (CFG)")
+ pp = gr.Checkbox(label="Preprocess Prompt", value=True)
+ po = gr.Checkbox(label="Postprocess Output", value=True)
+ return ns, gs, dn, sp, du, pp, po
+
+theme = gr.themes.Soft(font=["Inter", "Arial", "sans-serif"])
+css = """
+.gradio-container {max-width: 100% !important; font-size: 16px !important;}
+.gradio-container h1 {font-size: 1.5em !important;}
+.gradio-container .prose {font-size: 1.1em !important;}
+.compact-audio audio {height: 60px !important;}
+.compact-audio .waveform {min-height: 80px !important;}
+"""
+
+with gr.Blocks(theme=theme, css=css, title="OmniVoice Local Client UI") as demo:
+ gr.Markdown(
+ """
+ # OmniVoice Local Client UI
+
+ This UI is connected natively against the local FastAPI (`api.py`) instance at `http://localhost:8000/generate`.
+ """
+ )
+
+ with gr.Tabs():
+ # ================= Voice Clone Tab =================
+ with gr.TabItem("Voice Clone"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vc_text = gr.Textbox(label="Text to Synthesize / 待合成文本", lines=4)
+ vc_ref_audio = gr.Audio(label="Reference Audio / 参考音频", type="filepath", elem_classes="compact-audio")
+ vc_ref_text = gr.Textbox(label="Reference Text (optional) / 参考音频文本(可选)", lines=2)
+ vc_lang = _lang_dropdown()
+
+ with gr.Accordion("Instruct (optional)", open=False):
+ vc_instruct = gr.Textbox(label="Instruct", lines=2)
+
+ vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po = _gen_settings()
+ vc_btn = gr.Button("Generate / 生成", variant="primary")
+
+ with gr.Column(scale=1):
+ vc_audio = gr.Audio(label="Output Audio / 合成结果", type="filepath")
+ vc_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def clone_wrapper(text, lang, ref_aud, ref_text, instruct, ns, gs, dn, sp, du, pp, po):
+ if not ref_aud:
+ return None, "Error: Must provide reference audio for Voice Clone."
+
+ try:
+ files = {}
+ if ref_aud and os.path.exists(ref_aud):
+ files["ref_audio"] = open(ref_aud, 'rb')
+
+ data = {
+ "text": text or "",
+ "language": lang if lang != "Auto" else "",
+ "ref_text": ref_text or "",
+ "instruct": instruct or "",
+ "num_step": int(ns),
+ "guidance_scale": float(gs),
+ "denoise": dn,
+ "speed": float(sp),
+ "t_shift": 0.1,
+ "postprocess_output": po,
+ "layer_penalty_factor": 5.0,
+ "position_temperature": 5.0,
+ "class_temperature": 0.0,
+ }
+ if du > 0:
+ data["duration"] = float(du)
+
+ resp = requests.post(API_URL, data=data, files=files)
+
+ # Cleanup file handle
+ if "ref_audio" in files:
+ files["ref_audio"].close()
+
+ if resp.status_code == 200:
+ # Write stream back to file for UI to ingest
+ fd, path = tempfile.mkstemp(suffix=".wav")
+ with os.fdopen(fd, 'wb') as f:
+ f.write(resp.content)
+ return path, "Success!"
+ else:
+ return None, f"API Error [{resp.status_code}]: {resp.text}"
+ except Exception as e:
+ return None, f"Local Request Error: {e}"
+
+ vc_btn.click(
+ clone_wrapper,
+ inputs=[vc_text, vc_lang, vc_ref_audio, vc_ref_text, vc_instruct, vc_ns, vc_gs, vc_dn, vc_sp, vc_du, vc_pp, vc_po],
+ outputs=[vc_audio, vc_status]
+ )
+
+ # ================= Voice Design Tab =================
+ with gr.TabItem("Voice Design"):
+ with gr.Row():
+ with gr.Column(scale=1):
+ vd_text = gr.Textbox(label="Text to Synthesize / 待合成文本", lines=4)
+ vd_lang = _lang_dropdown()
+
+ vd_groups = []
+ for _cat, _choices in _CATEGORIES.items():
+ vd_groups.append(
+ gr.Dropdown(
+ label=_cat,
+ choices=["Auto"] + _choices,
+ value="Auto",
+ info=_ATTR_INFO.get(_cat),
+ )
+ )
+
+ vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po = _gen_settings()
+ vd_btn = gr.Button("Generate / 生成", variant="primary")
+
+ with gr.Column(scale=1):
+ vd_audio = gr.Audio(label="Output Audio / 合成结果", type="filepath")
+ vd_status = gr.Textbox(label="Status / 状态", lines=2)
+
+ def _build_instruct(groups):
+ selected = [g for g in groups if g and g != "Auto"]
+ if not selected:
+ return None
+ parts = []
+ for v in selected:
+ if " / " in v:
+ en, zh = v.split(" / ", 1)
+ if "Dialect" in v.split(" / ")[0]:
+ parts.append(zh.strip())
+ else:
+ parts.append(en.strip())
+ else:
+ parts.append(v)
+ return ", ".join(parts)
+
+ def design_wrapper(text, lang, ns, gs, dn, sp, du, pp, po, *groups):
+ try:
+ instruct_str = _build_instruct(groups)
+
+ data = {
+ "text": text or "",
+ "language": lang if lang != "Auto" else "",
+ "instruct": instruct_str or "",
+ "num_step": int(ns),
+ "guidance_scale": float(gs),
+ "denoise": dn,
+ "speed": float(sp),
+ "t_shift": 0.1,
+ "postprocess_output": po,
+ "layer_penalty_factor": 5.0,
+ "position_temperature": 5.0,
+ "class_temperature": 0.0,
+ }
+ if du > 0:
+ data["duration"] = float(du)
+
+ resp = requests.post(API_URL, data=data)
+ if resp.status_code == 200:
+ fd, path = tempfile.mkstemp(suffix=".wav")
+ with os.fdopen(fd, 'wb') as f:
+ f.write(resp.content)
+ return path, "Success!"
+ else:
+ return None, f"API Error [{resp.status_code}]: {resp.text}"
+ except Exception as e:
+ return None, f"Local Request Error: {e}"
+
+ vd_btn.click(
+ design_wrapper,
+ inputs=[vd_text, vd_lang, vd_ns, vd_gs, vd_dn, vd_sp, vd_du, vd_pp, vd_po] + vd_groups,
+ outputs=[vd_audio, vd_status]
+ )
+
+if __name__ == "__main__":
+ demo.launch(server_name="0.0.0.0", server_port=7861)
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 00000000..fe4ae7f7
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,3484 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+
+[manifest]
+constraints = [
+ { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = "==2.8.0" },
+ { name = "torch", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = "==2.8.0", index = "https://download.pytorch.org/whl/cu128" },
+ { name = "torchaudio", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = "==2.8.0" },
+ { name = "torchaudio", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = "==2.8.0", index = "https://download.pytorch.org/whl/cu128" },
+]
+
+[[package]]
+name = "accelerate"
+version = "1.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "safetensors" },
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" },
+]
+
+[[package]]
+name = "aiofiles"
+version = "24.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" },
+]
+
+[[package]]
+name = "aliyun-python-sdk-core"
+version = "2.16.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "jmespath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" }
+
+[[package]]
+name = "aliyun-python-sdk-kms"
+version = "2.16.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aliyun-python-sdk-core" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" },
+]
+
+[[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.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "audioop-lts"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" },
+ { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" },
+ { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" },
+ { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" },
+ { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" },
+ { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" },
+ { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" },
+ { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" },
+ { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" },
+ { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" },
+ { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" },
+ { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" },
+ { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" },
+ { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" },
+ { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
+]
+
+[[package]]
+name = "audioread"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "standard-aifc", marker = "python_full_version >= '3.13'" },
+ { name = "standard-sunau", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/4a/874ecf9b472f998130c2b5e145dcdb9f6131e84786111489103b66772143/audioread-3.1.0.tar.gz", hash = "sha256:1c4ab2f2972764c896a8ac61ac53e261c8d29f0c6ccd652f84e18f08a4cab190", size = 20082, upload-time = "2025-10-26T19:44:13.484Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/16/fbe8e1e185a45042f7cd3a282def5bb8d95bb69ab9e9ef6a5368aa17e426/audioread-3.1.0-py3-none-any.whl", hash = "sha256:b30d1df6c5d3de5dcef0fb0e256f6ea17bdcf5f979408df0297d8a408e2971b4", size = 23143, upload-time = "2025-10-26T19:44:12.016Z" },
+]
+
+[[package]]
+name = "braceexpand"
+version = "0.1.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/93/badd4f5ccf25209f3fef2573073da9fe4a45a3da99fca2f800f942130c0f/braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705", size = 7777, upload-time = "2021-05-07T13:49:07.323Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/93/e8c04e80e82391a6e51f218ca49720f64236bc824e92152a2633b74cf7ab/braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014", size = 5923, upload-time = "2021-05-07T13:49:05.146Z" },
+]
+
+[[package]]
+name = "brotli"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" },
+ { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" },
+ { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" },
+ { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" },
+ { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" },
+ { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" },
+ { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" },
+ { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" },
+ { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" },
+ { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" },
+ { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" },
+ { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" },
+ { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
+ { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
+ { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
+ { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
+ { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
+ { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.2.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
+]
+
+[[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" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" },
+ { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" },
+ { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" },
+ { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" },
+ { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" },
+ { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" },
+ { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" },
+ { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" },
+ { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" },
+ { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" },
+ { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" },
+ { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" },
+ { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" },
+ { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" },
+ { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" },
+ { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
+ { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
+ { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
+ { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
+ { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
+ { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
+ { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
+ { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
+ { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
+ { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
+ { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
+ { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
+ { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
+ { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[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 = "crcmod"
+version = "1.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" }
+
+[[package]]
+name = "cryptography"
+version = "46.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
+ { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
+ { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
+ { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
+ { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
+ { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
+ { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
+ { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
+ { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
+ { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
+ { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
+ { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
+ { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "editdistance"
+version = "0.8.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz", hash = "sha256:d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed", size = 50006, upload-time = "2024-02-10T07:44:53.914Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/c9/302658ce7f4c537a4e85cf578d11bbf7af120a712e1d78fedc6cb8823c65/editdistance-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:adeb705f32b93accc74960d227875abff150ee42d676e428536361fe5f8f5388", size = 106150, upload-time = "2024-02-10T07:43:15.903Z" },
+ { url = "https://files.pythonhosted.org/packages/45/80/0b3c7d2c0e183725986fea5dd2df11f0b4b46320e9a64f6077a121ab1f64/editdistance-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3de77951b105d0972deec7684a0b3d1a9dee69c9b5d34f6e2acc0d76cd4a1c52", size = 80551, upload-time = "2024-02-10T07:43:17.64Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/14/681460965c6a4a48321b07f88de2273d097fdca0491ff55db891aacbd291/editdistance-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e88efb052d45e924606c305cb833a80579dca3e8e4ff01309d50ba2c1c0bbd5", size = 79142, upload-time = "2024-02-10T07:43:19.195Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/0d/abdbc8e394a9461cf2ae27c16564fadaa65f52bd242dd1582ae5e7736dc3/editdistance-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0247e7a1e9c66ea75211a97e725366bff19a52aac2c838ed5f90025630e976dd", size = 396768, upload-time = "2024-02-10T07:43:20.912Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/fb/2940d26ebda12efd280ae939436f17ac482930d862df9e774cb8b771ab03/editdistance-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d143429a49ab552411505f550a0fb4285a1d4336e096804d233ec495ac20fc", size = 401846, upload-time = "2024-02-10T07:43:23.169Z" },
+ { url = "https://files.pythonhosted.org/packages/53/cc/c63d75c7f387d4df0645682c1ab8706c2dfe5c9c0c4999723ce9a3ba0853/editdistance-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9d3be2b10e5d44a950a4bd1e84bca9ebbecd364bce0cf5693bf8224c78eaef", size = 397543, upload-time = "2024-02-10T07:43:24.621Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/38/bb0f734a7571e093184606b930734b12da5b6bff2635eba9312fe4536dd9/editdistance-0.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5c72aa1df8535f2e2b3d8773a1a7da091bc1a7e52bb396e7e48d375ba687e7b2", size = 898934, upload-time = "2024-02-10T07:43:26.926Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9f/624fc7a09918f850a057465f02e86f269e139a457f48ff8cabfb12701756/editdistance-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a606c34a2a6cc190e4fffc856b36333cdcf1f1fab5b22bd3088e585c22d6ca0", size = 959637, upload-time = "2024-02-10T07:43:28.997Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/5c/7fa6cc277f91c477ee370807d51c1826891dc6dfc307544223ce7f2687de/editdistance-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5af173d442ffac33b7c7990132f97f88818a3abf4b21c0c702a7022df37c0c5c", size = 911024, upload-time = "2024-02-10T07:43:30.449Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/97/556215f71184291155aee340a6d34f0676e7238fdfd10615b6b775ce25fe/editdistance-0.8.1-cp310-cp310-win32.whl", hash = "sha256:fd64b58f5a7b59afd9d75982aaeeacd2a98498bf472fa0360c122ffe6ea4c871", size = 80834, upload-time = "2024-02-10T07:43:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/d1/7ec5f5cbb95838d0eff7f980a660c81acd1363d658f2f5d4ceba38877c5a/editdistance-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6c7c62c3cae45ca1fa01bb2722b297b9de1e3a244ac44cfba88bdcb488fe6aee", size = 79614, upload-time = "2024-02-10T07:43:33.255Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/dc/d0c29fd52d8f9e795653ed2b838a2a48c739cdfff04ac5b79c6c0ecbdf79/editdistance-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:486105603a273d73d12a54f347dffa70ab281749d7c3879658b377bc49e4b98c", size = 106079, upload-time = "2024-02-10T07:43:34.34Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c6/75fa45d7b78fbea6fd894f4e48895a75bd3c83d4a9a6b57673881d74d3e0/editdistance-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fad081f5f86a175c1a09a4e9e45b95c9349e454c21e181e842e01c85f1f536fc", size = 80580, upload-time = "2024-02-10T07:43:35.947Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/a3/058d823b6285c3511dc94ed80620c3fb0c18b4aaa708f70ba71f3af28436/editdistance-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cb78e125f6759398885a775f5eed07c2bb72b2f86da43e674c6b6a3335b273b", size = 79087, upload-time = "2024-02-10T07:43:36.923Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/3a/0b13c7864c93b1e9b9952bd2a33c5ef3c4fd1bf70a5fad6924789e70e5eb/editdistance-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3778ca60aa89def9144b70e330bcec5330c7da1d69cb28c612e90b84510a1d3d", size = 409296, upload-time = "2024-02-10T07:43:38.52Z" },
+ { url = "https://files.pythonhosted.org/packages/96/8a/db0fd79e8ddb9b5f86f274107c5d0a27ec4f2af88877df1f26c2c6d150cc/editdistance-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba945eaa0436cf40bc53d7e299dc537c7c71353379a095b7459ff4af910da33", size = 412913, upload-time = "2024-02-10T07:43:39.852Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/d2/98be7112750ff17b436dd76f988f1e38570dcec0df8578ee19ef046f22fe/editdistance-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:877f2a0d801f32bc1a1878901ffb947b974361e849c66e314a7f1d786a446b58", size = 407430, upload-time = "2024-02-10T07:43:41.048Z" },
+ { url = "https://files.pythonhosted.org/packages/03/62/1815e3bf164910c47ba1948c8b5e937a40c7f9763b64e98fb6666b01dd06/editdistance-0.8.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e79d351ca40a6ead5f3763253fd7521572ee0d3e5d42538630e56d10f48db481", size = 909217, upload-time = "2024-02-10T07:43:42.916Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d3/a832cea7b507a9be54e4ac3d1340fb66dca5f9c16c70bf38d5039e8fdede/editdistance-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70ed382b3052a51161bad0149d4665003bf3b949fce0b01bf1253a4cc1a88239", size = 969407, upload-time = "2024-02-10T07:43:44.912Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b4/db291d2a3845cbf8047b4b5aad3b3e038a8a2994d87027b40e1a1b0f4b74/editdistance-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a529bfb384c4000775d76739c4e64f73337f0f5a3784933b1321b577a62bed4e", size = 922112, upload-time = "2024-02-10T07:43:47.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/7ddeacada4982d0b892a28897e21871d0f25bca165e3663e37c3a272808a/editdistance-0.8.1-cp311-cp311-win32.whl", hash = "sha256:b082232429e731f181af7f7d2bcf79da6ca8fadd04e9086c11e2973f7d330c81", size = 80799, upload-time = "2024-02-10T07:43:48.231Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a1/778af8590b8b12f03f62eacc3c8744407ade9e3d69be6dabe38d0afbf2dd/editdistance-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:cef1a4359252a49f2c4718e64e9d40027d9d951b289d045bdb278656e59f6af8", size = 79698, upload-time = "2024-02-10T07:43:49.234Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/4c/7f195588949b4e72436dc7fc902632381f96e586af829685b56daebb38b8/editdistance-0.8.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04af61b3fcdd287a07c15b6ae3b02af01c5e3e9c3aca76b8c1d13bd266b6f57", size = 106723, upload-time = "2024-02-10T07:43:50.268Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/82/31dc1640d830cd7d36865098329f34e4dad3b77f31cfb9404b347e700196/editdistance-0.8.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:18fc8b6eaae01bfd9cf999af726c1e8dcf667d120e81aa7dbd515bea7427f62f", size = 80998, upload-time = "2024-02-10T07:43:51.259Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/2a/6b823e71cef694d6f070a1d82be2842706fa193541aab8856a8f42044cd0/editdistance-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a87839450a5987028738d061ffa5ef6a68bac2ddc68c9147a8aae9806629c7f", size = 79248, upload-time = "2024-02-10T07:43:52.873Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/31/bfb8e590f922089dc3471ed7828a6da2fc9453eba38c332efa9ee8749fd7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24b5f9c9673c823d91b5973d0af8b39f883f414a55ade2b9d097138acd10f31e", size = 415262, upload-time = "2024-02-10T07:43:54.498Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/57423942b2f847cdbbb46494568d00cd8a45500904ea026f0aad6ca01bc7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c59248eabfad603f0fba47b0c263d5dc728fb01c2b6b50fb6ca187cec547fdb3", size = 418905, upload-time = "2024-02-10T07:43:55.779Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/05/dfa4cdcce063596cbf0d7a32c46cd0f4fa70980311b7da64d35f33ad02a0/editdistance-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e239d88ff52821cf64023fabd06a1d9a07654f364b64bf1284577fd3a79d0e", size = 412511, upload-time = "2024-02-10T07:43:57.567Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/14/39608ff724a9523f187c4e28926d78bc68f2798f74777ac6757981108345/editdistance-0.8.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2f7f71698f83e8c83839ac0d876a0f4ef996c86c5460aebd26d85568d4afd0db", size = 917293, upload-time = "2024-02-10T07:43:59.559Z" },
+ { url = "https://files.pythonhosted.org/packages/df/92/4a1c61d72da40dedfd0ff950fdc71ae83f478330c58a8bccfd776518bd67/editdistance-0.8.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:04e229d6f4ce0c12abc9f4cd4023a5b5fa9620226e0207b119c3c2778b036250", size = 975580, upload-time = "2024-02-10T07:44:01.328Z" },
+ { url = "https://files.pythonhosted.org/packages/47/3d/9877566e724c8a37f2228a84ec5cbf66dbfd0673515baf68a0fe07caff40/editdistance-0.8.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e16721636da6d6b68a2c09eaced35a94f4a4a704ec09f45756d4fd5e128ed18d", size = 929121, upload-time = "2024-02-10T07:44:02.764Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/8c50757d198b8ca30ddb91e8b8f0247a8dca04ff2ec30755245f0ab1ff0c/editdistance-0.8.1-cp312-cp312-win32.whl", hash = "sha256:87533cf2ebc3777088d991947274cd7e1014b9c861a8aa65257bcdc0ee492526", size = 81039, upload-time = "2024-02-10T07:44:04.134Z" },
+ { url = "https://files.pythonhosted.org/packages/28/f0/65101e51dc7c850e7b7581a5d8fa8721a1d7479a0dca6c08386328e19882/editdistance-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:09f01ed51746d90178af7dd7ea4ebb41497ef19f53c7f327e864421743dffb0a", size = 79853, upload-time = "2024-02-10T07:44:05.687Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/4c/c9d02eeb47815d35f8d324b52f6704ea7beb032bcb209358cac44047d413/editdistance-0.8.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a4a90c6b03094c07358572027a8d0a13cca7450b1aa6caca98a5f1fa4f0b8961", size = 76455, upload-time = "2024-02-10T07:44:36.838Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b0/2818fa6a24595dac069b0bfb9d05658406779a1ded8fd2b0c9066396cf99/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510a4f9ced348a4fd89ae2e102357d4d801a771e29bb2bc2f130a1692193407f", size = 84104, upload-time = "2024-02-10T07:44:37.928Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/d1/3d5e09bcf7fdb7aed705bf74047a8634bd2b8fd92177c25a2547e6dbadfb/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4787fa7228ba6a34b430066d174320f011d605015baa7299c2c4911e6ea6bd46", size = 89058, upload-time = "2024-02-10T07:44:39.113Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/88/fca5d7b1a1edf66ce1e5b6b60bff75842e6814b4f5facbdf4585d88c912d/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee02601375073afccd6b4d811129ce1cb696d47db734784d8dbd1fddcea75447", size = 84635, upload-time = "2024-02-10T07:44:40.714Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/91/0e6285bbe2358d81fd16313d30306b2d0036387348f7bc11d8c076ca3c72/editdistance-0.8.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bc7ad9f9a20e6f351523de77c59249f005242e3f317b5de45d02c378d24f6531", size = 77389, upload-time = "2024-02-10T07:44:41.725Z" },
+]
+
+[[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 = "fastapi"
+version = "0.135.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" },
+]
+
+[[package]]
+name = "ffmpy"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.25.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" },
+]
+
+[[package]]
+name = "funasr"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "editdistance" },
+ { name = "hydra-core" },
+ { name = "jaconv" },
+ { name = "jamo" },
+ { name = "jieba" },
+ { name = "kaldiio" },
+ { name = "librosa" },
+ { name = "modelscope" },
+ { name = "oss2" },
+ { name = "pytorch-wpe" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "sentencepiece" },
+ { name = "soundfile" },
+ { name = "tensorboardx" },
+ { name = "torch-complex" },
+ { name = "tqdm" },
+ { name = "umap-learn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/87/dc/d5586cd788bdf75286d73e646f5f354f8b3220b75a61c0325a177689065d/funasr-1.3.1.tar.gz", hash = "sha256:ed813c0ecade7d24393943a82a91f7cee822178ab4bfd303adb8ad318605e0a5", size = 664362, upload-time = "2026-01-26T13:07:43.579Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/89/61c09967f0f4f091402367215000c3c060bfeaadbbdc9aca42856d299c95/funasr-1.3.1-py3-none-any.whl", hash = "sha256:f63050d7d625f287ec741b84a0325365699c49550a4bfd3ace4acb43e41a87a8", size = 811975, upload-time = "2026-01-26T13:07:41.866Z" },
+]
+
+[[package]]
+name = "gradio"
+version = "6.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiofiles" },
+ { name = "anyio" },
+ { name = "audioop-lts", marker = "python_full_version >= '3.13'" },
+ { name = "brotli" },
+ { name = "fastapi" },
+ { name = "ffmpy" },
+ { name = "gradio-client" },
+ { name = "groovy" },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "orjson" },
+ { name = "packaging" },
+ { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "pandas", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pillow" },
+ { name = "pydantic" },
+ { name = "pydub" },
+ { name = "python-multipart" },
+ { name = "pytz" },
+ { name = "pyyaml" },
+ { name = "safehttpx" },
+ { name = "semantic-version" },
+ { name = "starlette" },
+ { name = "tomlkit" },
+ { name = "typer" },
+ { name = "typing-extensions" },
+ { name = "uvicorn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/83/29bdbf94b212512e3c775482d390f5b699a72d71a2c431dea367a6e45a37/gradio-6.9.0.tar.gz", hash = "sha256:593e60e33233f3586452ebfa9f741817c5ae849a98cc70945f3ccb8dc895eb22", size = 57904480, upload-time = "2026-03-06T17:44:26.025Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/8b/dc357ab966544e4dc898a2fee326d755c5f54da82af71a1a802e3476e78e/gradio-6.9.0-py3-none-any.whl", hash = "sha256:c173dd330c9247002a42222c85d76c0ecee65437eff808084e360862e7bbd24f", size = 42940853, upload-time = "2026-03-06T17:44:22.009Z" },
+]
+
+[[package]]
+name = "gradio-client"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fsspec" },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "packaging" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/d2/de2037f5eff13a5145cdf6982fd34c9735f0806e8a2ee5d4bfe9a7d25a54/gradio_client-2.3.0.tar.gz", hash = "sha256:1c700dc60e65bae4386ba7cf3732b9f9d5bcf5fb8eb451df3944fe092d7d9a29", size = 57552, upload-time = "2026-03-06T17:44:38.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/6a/41752781399811afbf8ac858f63c20eff354ed35169daa39604aefced4e8/gradio_client-2.3.0-py3-none-any.whl", hash = "sha256:9ec51a927888fc188e123a0ac5ad341d9265b325539a399554d1fc2604942e74", size = 58531, upload-time = "2026-03-06T17:44:36.961Z" },
+]
+
+[[package]]
+name = "groovy"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" },
+]
+
+[[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.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" },
+ { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" },
+ { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" },
+ { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" },
+ { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" },
+ { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" },
+ { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" },
+ { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" },
+]
+
+[[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.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { 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/19/15/eafc1c57bf0f8afffb243dcd4c0cceb785e956acc17bba4d9bf2ae21fc9c/huggingface_hub-1.7.2.tar.gz", hash = "sha256:7f7e294e9bbb822e025bdb2ada025fa4344d978175a7f78e824d86e35f7ab43b", size = 724684, upload-time = "2026-03-20T10:36:08.767Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" },
+]
+
+[[package]]
+name = "hydra-core"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "antlr4-python3-runtime" },
+ { name = "omegaconf" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "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 = "jieba"
+version = "0.42.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" }
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+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 = "jiwer"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rapidfuzz" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/3e/71b95cf0e2179fb5de8744a79fd36c8bd4e02e1803129a16d423884b6654/jiwer-3.1.0.tar.gz", hash = "sha256:dc492d09e570f1baba98c76aba09baf8e09c06e6808a4ba412dd4bde67fb79ac", size = 103187, upload-time = "2025-01-31T12:14:10.86Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/f4/35634d9eeff3b0bab51f5b9474ee569b1186bf29cf0d9d67b84acc80c53d/jiwer-3.1.0-py3-none-any.whl", hash = "sha256:5a14b5bba4692e1946ca3c6946435f7d90b1b526076ccb6c12be763e2146237d", size = 22303, upload-time = "2025-01-31T12:14:08.893Z" },
+]
+
+[[package]]
+name = "jmespath"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
+]
+
+[[package]]
+name = "kaldiio"
+version = "2.18.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/85/92435e8e62eb3d43eded9f24643fc2a6dbce031cebceed11528147c7873f/kaldiio-2.18.1.tar.gz", hash = "sha256:0283d197fac6ac683f7a9e6af8d18aad9dbd2c4a997f22e45294f2ac1ee3c432", size = 35570, upload-time = "2025-03-06T15:57:52.375Z" }
+wheels = [
+ { 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.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pooch" },
+ { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "soundfile" },
+ { name = "soxr" },
+ { name = "standard-aifc", marker = "python_full_version >= '3.13'" },
+ { name = "standard-sunau", marker = "python_full_version >= '3.13'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/36/360b5aafa0238e29758729e9486c6ed92a6f37fa403b7875e06c115cdf4a/librosa-0.11.0.tar.gz", hash = "sha256:f5ed951ca189b375bbe2e33b2abd7e040ceeee302b9bbaeeffdfddb8d0ace908", size = 327001, upload-time = "2025-03-11T15:09:54.884Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" },
+]
+
+[[package]]
+name = "llvmlite"
+version = "0.46.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" },
+ { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" },
+ { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[[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" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[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.35.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/3d/fc/5822a2fa4f16054a74edf7949090cda86c8e80b3fa6e52d726a17caf2bb1/modelscope-1.35.1.tar.gz", hash = "sha256:b68eb8a8169f74766c3a7d8a95805714174d082d5568d6b281740536e7cc9f19", size = 4561746, upload-time = "2026-03-19T06:53:02.769Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/d3/c98f736bbb5739871214e567ef9a1f5fca65f10d1b7bdc5e1bd565d492cf/modelscope-1.35.1-py3-none-any.whl", hash = "sha256:364db742867988da6be0493e0b9c4fd3e13bb0f5dd230c0c928102775aeed375", size = 6053743, upload-time = "2026-03-19T06:52:59.37Z" },
+]
+
+[[package]]
+name = "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.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
+ { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
+ { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
+ { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
+ { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" },
+ { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" },
+ { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" },
+ { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+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.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+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.64.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "llvmlite" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/23/c9/a0fb41787d01d621046138da30f6c2100d80857bf34b3390dd68040f27a3/numba-0.64.0.tar.gz", hash = "sha256:95e7300af648baa3308127b1955b52ce6d11889d16e8cfe637b4f85d2fca52b1", size = 2765679, upload-time = "2026-02-18T18:41:20.974Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/5e/604fed821cd7e3426bb3bc99a7ed6ac0bcb489f4cd93052256437d082f95/numba-0.64.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc09b79440952e3098eeebea4bf6e8d2355fb7f12734fcd9fc5039f0dca90727", size = 2683250, upload-time = "2026-02-18T18:40:45.829Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/9f/9275a723d050b5f1a9b1c7fb7dbfce324fef301a8e50c5f88338569db06c/numba-0.64.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1afe3a80b8c2f376b211fb7a49e536ef9eafc92436afc95a2f41ea5392f8cc65", size = 3742168, upload-time = "2026-02-18T18:40:48.066Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/d1/97ca7dddaa36b16f4c46319bdb6b4913ba15d0245317d0d8ccde7b2d7d92/numba-0.64.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23804194b93b8cd416c6444b5fbc4956082a45fed2d25436ef49c594666e7f7e", size = 3449103, upload-time = "2026-02-18T18:40:49.905Z" },
+ { url = "https://files.pythonhosted.org/packages/52/0a/b9e137ad78415373e3353564500e8bf29dbce3c0d73633bb384d4e5d7537/numba-0.64.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2a9fe998bb2cf848960b34db02c2c3b5e02cf82c07a26d9eef3494069740278", size = 2749950, upload-time = "2026-02-18T18:40:51.536Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a3/1a4286a1c16136c8896d8e2090d950e79b3ec626d3a8dc9620f6234d5a38/numba-0.64.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:766156ee4b8afeeb2b2e23c81307c5d19031f18d5ce76ae2c5fb1429e72fa92b", size = 2682938, upload-time = "2026-02-18T18:40:52.897Z" },
+ { url = "https://files.pythonhosted.org/packages/19/16/aa6e3ba3cd45435c117d1101b278b646444ed05b7c712af631b91353f573/numba-0.64.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d17071b4ffc9d39b75d8e6c101a36f0c81b646123859898c9799cb31807c8f78", size = 3747376, upload-time = "2026-02-18T18:40:54.925Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/f1/dd2f25e18d75fdf897f730b78c5a7b00cc4450f2405564dbebfaf359f21f/numba-0.64.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ead5630434133bac87fa67526eacb264535e4e9a2d5ec780e0b4fc381a7d275", size = 3453292, upload-time = "2026-02-18T18:40:56.818Z" },
+ { url = "https://files.pythonhosted.org/packages/31/29/e09d5630578a50a2b3fa154990b6b839cf95327aa0709e2d50d0b6816cd1/numba-0.64.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2b1fd93e7aaac07d6fbaed059c00679f591f2423885c206d8c1b55d65ca3f2d", size = 2749824, upload-time = "2026-02-18T18:40:58.392Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a6/9fc52cb4f0d5e6d8b5f4d81615bc01012e3cf24e1052a60f17a68deb8092/numba-0.64.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69440a8e8bc1a81028446f06b363e28635aa67bd51b1e498023f03b812e0ce68", size = 2683418, upload-time = "2026-02-18T18:40:59.886Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/89/1a74ea99b180b7a5587b0301ed1b183a2937c4b4b67f7994689b5d36fc34/numba-0.64.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13721011f693ba558b8dd4e4db7f2640462bba1b855bdc804be45bbeb55031a", size = 3804087, upload-time = "2026-02-18T18:41:01.699Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e1/583c647404b15f807410510fec1eb9b80cb8474165940b7749f026f21cbc/numba-0.64.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0b180b1133f2b5d8b3f09d96b6d7a9e51a7da5dda3c09e998b5bcfac85d222c", size = 3504309, upload-time = "2026-02-18T18:41:03.252Z" },
+ { url = "https://files.pythonhosted.org/packages/85/23/0fce5789b8a5035e7ace21216a468143f3144e02013252116616c58339aa/numba-0.64.0-cp312-cp312-win_amd64.whl", hash = "sha256:e63dc94023b47894849b8b106db28ccb98b49d5498b98878fac1a38f83ac007a", size = 2752740, upload-time = "2026-02-18T18:41:05.097Z" },
+ { url = "https://files.pythonhosted.org/packages/52/80/2734de90f9300a6e2503b35ee50d9599926b90cbb7ac54f9e40074cd07f1/numba-0.64.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3bab2c872194dcd985f1153b70782ec0fbbe348fffef340264eacd3a76d59fd6", size = 2683392, upload-time = "2026-02-18T18:41:06.563Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e8/14b5853ebefd5b37723ef365c5318a30ce0702d39057eaa8d7d76392859d/numba-0.64.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703a246c60832cad231d2e73c1182f25bf3cc8b699759ec8fe58a2dbc689a70c", size = 3812245, upload-time = "2026-02-18T18:41:07.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/a2/f60dc6c96d19b7185144265a5fbf01c14993d37ff4cd324b09d0212aa7ce/numba-0.64.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2e49a7900ee971d32af7609adc0cfe6aa7477c6f6cccdf6d8138538cf7756f", size = 3511328, upload-time = "2026-02-18T18:41:09.504Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/2a/fe7003ea7e7237ee7014f8eaeeb7b0d228a2db22572ca85bab2648cf52cb/numba-0.64.0-cp313-cp313-win_amd64.whl", hash = "sha256:396f43c3f77e78d7ec84cdfc6b04969c78f8f169351b3c4db814b97e7acf4245", size = 2752668, upload-time = "2026-02-18T18:41:11.455Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8a/77d26afe0988c592dd97cb8d4e80bfb3dfc7dbdacfca7d74a7c5c81dd8c2/numba-0.64.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f565d55eaeff382cbc86c63c8c610347453af3d1e7afb2b6569aac1c9b5c93ce", size = 2683590, upload-time = "2026-02-18T18:41:12.897Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/4b/600b8b7cdbc7f9cebee9ea3d13bb70052a79baf28944024ffcb59f0712e3/numba-0.64.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b55169b18892c783f85e9ad9e6f5297a6d12967e4414e6b71361086025ff0bb", size = 3781163, upload-time = "2026-02-18T18:41:15.377Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/73/53f2d32bfa45b7175e9944f6b816d8c32840178c3eee9325033db5bf838e/numba-0.64.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:196bcafa02c9dd1707e068434f6d5cedde0feb787e3432f7f1f0e993cc336c4c", size = 3481172, upload-time = "2026-02-18T18:41:17.281Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/00/aebd2f7f1e11e38814bb96e95a27580817a7b340608d3ac085fdbab83174/numba-0.64.0-cp314-cp314-win_amd64.whl", hash = "sha256:213e9acbe7f1c05090592e79020315c1749dd52517b90e94c517dca3f014d4a1", size = 2754700, upload-time = "2026-02-18T18:41:19.277Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+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/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
+ { 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.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
+ { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
+ { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
+ { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
+ { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
+ { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
+ { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
+ { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
+ { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
+ { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
+ { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
+ { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
+ { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
+ { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
+ { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
+ { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
+ { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
+ { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
+ { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
+]
+
+[[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", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+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", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+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", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+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", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+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.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/5b/4e4fff7bad39adf89f735f2bc87248c81db71205b62bcc0d5ca5b606b3c3/nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adf27ccf4238253e0b826bce3ff5fa532d65fc42322c8bfdfaf28024c0fbe039", size = 322364134, upload-time = "2025-06-03T21:58:04.013Z" },
+]
+
+[[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-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.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "antlr4-python3-runtime" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" },
+]
+
+[[package]]
+name = "omnivoice"
+version = "0.1.3"
+source = { editable = "." }
+dependencies = [
+ { name = "accelerate" },
+ { name = "gradio" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pydub" },
+ { name = "soundfile" },
+ { name = "tensorboardx" },
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "torchaudio", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "transformers" },
+ { name = "webdataset" },
+]
+
+[package.optional-dependencies]
+api = [
+ { name = "fastapi" },
+ { name = "python-multipart" },
+ { name = "uvicorn" },
+]
+eval = [
+ { name = "funasr" },
+ { name = "jiwer" },
+ { name = "librosa" },
+ { name = "s3prl" },
+ { name = "unidecode" },
+ { name = "zhconv" },
+ { name = "zhon" },
+]
+ui = [
+ { name = "gradio" },
+ { name = "gradio-client" },
+ { name = "requests" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "accelerate" },
+ { name = "fastapi", marker = "extra == 'api'" },
+ { name = "funasr", marker = "extra == 'eval'" },
+ { name = "gradio" },
+ { name = "gradio", marker = "extra == 'ui'" },
+ { name = "gradio-client", marker = "extra == 'ui'" },
+ { name = "jiwer", marker = "extra == 'eval'", specifier = "==3.1.0" },
+ { name = "librosa", marker = "extra == 'eval'" },
+ { name = "numpy" },
+ { name = "pydub" },
+ { name = "python-multipart", marker = "extra == 'api'" },
+ { name = "requests", marker = "extra == 'ui'" },
+ { name = "s3prl", marker = "extra == 'eval'" },
+ { name = "soundfile" },
+ { name = "tensorboardx" },
+ { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = ">=2.4" },
+ { name = "torch", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = ">=2.4", index = "https://download.pytorch.org/whl/cu128" },
+ { name = "torchaudio", marker = "sys_platform != 'linux' and sys_platform != 'win32'", specifier = ">=2.4" },
+ { name = "torchaudio", marker = "sys_platform == 'linux' or sys_platform == 'win32'", specifier = ">=2.4", index = "https://download.pytorch.org/whl/cu128" },
+ { name = "transformers", specifier = ">=5.3.0" },
+ { name = "unidecode", marker = "extra == 'eval'" },
+ { name = "uvicorn", marker = "extra == 'api'" },
+ { name = "webdataset" },
+ { name = "zhconv", marker = "extra == 'eval'" },
+ { name = "zhon", marker = "extra == 'eval'" },
+]
+provides-extras = ["eval", "api", "ui"]
+
+[[package]]
+name = "orjson"
+version = "3.11.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" },
+ { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" },
+ { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" },
+ { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" },
+ { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" },
+ { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" },
+ { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" },
+ { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" },
+ { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" },
+ { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" },
+ { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" },
+ { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" },
+ { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" },
+ { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" },
+ { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" },
+ { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" },
+ { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" },
+ { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" },
+ { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" },
+ { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" },
+ { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" },
+ { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" },
+ { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" },
+]
+
+[[package]]
+name = "oss2"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aliyun-python-sdk-core" },
+ { name = "aliyun-python-sdk-kms" },
+ { name = "crcmod" },
+ { name = "pycryptodome" },
+ { name = "requests" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" }
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version < '3.11'" },
+ { name = "pytz", marker = "python_full_version < '3.11'" },
+ { name = "tzdata", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil", marker = "python_full_version >= '3.11'" },
+ { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/0c/b28ed414f080ee0ad153f848586d61d1878f91689950f037f976ce15f6c8/pandas-3.0.1.tar.gz", hash = "sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8", size = 4641901, upload-time = "2026-02-17T22:20:16.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ff/07/c7087e003ceee9b9a82539b40414ec557aa795b584a1a346e89180853d79/pandas-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea", size = 10323380, upload-time = "2026-02-17T22:18:16.133Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/27/90683c7122febeefe84a56f2cde86a9f05f68d53885cebcc473298dfc33e/pandas-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796", size = 9923455, upload-time = "2026-02-17T22:18:19.13Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/f1/ed17d927f9950643bc7631aa4c99ff0cc83a37864470bc419345b656a41f/pandas-3.0.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389", size = 10753464, upload-time = "2026-02-17T22:18:21.134Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/7c/870c7e7daec2a6c7ff2ac9e33b23317230d4e4e954b35112759ea4a924a7/pandas-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7", size = 11255234, upload-time = "2026-02-17T22:18:24.175Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/39/3653fe59af68606282b989c23d1a543ceba6e8099cbcc5f1d506a7bae2aa/pandas-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf", size = 11767299, upload-time = "2026-02-17T22:18:26.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/31/1daf3c0c94a849c7a8dab8a69697b36d313b229918002ba3e409265c7888/pandas-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447", size = 12333292, upload-time = "2026-02-17T22:18:28.996Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/67/af63f83cd6ca603a00fe8530c10a60f0879265b8be00b5930e8e78c5b30b/pandas-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79", size = 9892176, upload-time = "2026-02-17T22:18:31.79Z" },
+ { url = "https://files.pythonhosted.org/packages/79/ab/9c776b14ac4b7b4140788eca18468ea39894bc7340a408f1d1e379856a6b/pandas-3.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1", size = 9151328, upload-time = "2026-02-17T22:18:35.721Z" },
+ { url = "https://files.pythonhosted.org/packages/37/51/b467209c08dae2c624873d7491ea47d2b47336e5403309d433ea79c38571/pandas-3.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d", size = 10344357, upload-time = "2026-02-17T22:18:38.262Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f1/e2567ffc8951ab371db2e40b2fe068e36b81d8cf3260f06ae508700e5504/pandas-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955", size = 9884543, upload-time = "2026-02-17T22:18:41.476Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/39/327802e0b6d693182403c144edacbc27eb82907b57062f23ef5a4c4a5ea7/pandas-3.0.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b", size = 10396030, upload-time = "2026-02-17T22:18:43.822Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/fe/89d77e424365280b79d99b3e1e7d606f5165af2f2ecfaf0c6d24c799d607/pandas-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4", size = 10876435, upload-time = "2026-02-17T22:18:45.954Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a6/2a75320849dd154a793f69c951db759aedb8d1dd3939eeacda9bdcfa1629/pandas-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1", size = 11405133, upload-time = "2026-02-17T22:18:48.533Z" },
+ { url = "https://files.pythonhosted.org/packages/58/53/1d68fafb2e02d7881df66aa53be4cd748d25cbe311f3b3c85c93ea5d30ca/pandas-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821", size = 11932065, upload-time = "2026-02-17T22:18:50.837Z" },
+ { url = "https://files.pythonhosted.org/packages/75/08/67cc404b3a966b6df27b38370ddd96b3b023030b572283d035181854aac5/pandas-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43", size = 9741627, upload-time = "2026-02-17T22:18:53.905Z" },
+ { url = "https://files.pythonhosted.org/packages/86/4f/caf9952948fb00d23795f09b893d11f1cacb384e666854d87249530f7cbe/pandas-3.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7", size = 9052483, upload-time = "2026-02-17T22:18:57.31Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/48/aad6ec4f8d007534c091e9a7172b3ec1b1ee6d99a9cbb936b5eab6c6cf58/pandas-3.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262", size = 10317509, upload-time = "2026-02-17T22:18:59.498Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/14/5990826f779f79148ae9d3a2c39593dc04d61d5d90541e71b5749f35af95/pandas-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56", size = 9860561, upload-time = "2026-02-17T22:19:02.265Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/80/f01ff54664b6d70fed71475543d108a9b7c888e923ad210795bef04ffb7d/pandas-3.0.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e", size = 10365506, upload-time = "2026-02-17T22:19:05.017Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/85/ab6d04733a7d6ff32bfc8382bf1b07078228f5d6ebec5266b91bfc5c4ff7/pandas-3.0.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791", size = 10873196, upload-time = "2026-02-17T22:19:07.204Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a9/9301c83d0b47c23ac5deab91c6b39fd98d5b5db4d93b25df8d381451828f/pandas-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a", size = 11370859, upload-time = "2026-02-17T22:19:09.436Z" },
+ { url = "https://files.pythonhosted.org/packages/59/fe/0c1fc5bd2d29c7db2ab372330063ad555fb83e08422829c785f5ec2176ca/pandas-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8", size = 11924584, upload-time = "2026-02-17T22:19:11.562Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/7d/216a1588b65a7aa5f4535570418a599d943c85afb1d95b0876fc00aa1468/pandas-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25", size = 9742769, upload-time = "2026-02-17T22:19:13.926Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/cb/810a22a6af9a4e97c8ab1c946b47f3489c5bca5adc483ce0ffc84c9cc768/pandas-3.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59", size = 9043855, upload-time = "2026-02-17T22:19:16.09Z" },
+ { url = "https://files.pythonhosted.org/packages/92/fa/423c89086cca1f039cf1253c3ff5b90f157b5b3757314aa635f6bf3e30aa/pandas-3.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06", size = 10752673, upload-time = "2026-02-17T22:19:18.304Z" },
+ { url = "https://files.pythonhosted.org/packages/22/23/b5a08ec1f40020397f0faba72f1e2c11f7596a6169c7b3e800abff0e433f/pandas-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f", size = 10404967, upload-time = "2026-02-17T22:19:20.726Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/81/94841f1bb4afdc2b52a99daa895ac2c61600bb72e26525ecc9543d453ebc/pandas-3.0.1-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324", size = 10320575, upload-time = "2026-02-17T22:19:24.919Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/8b/2ae37d66a5342a83adadfd0cb0b4bf9c3c7925424dd5f40d15d6cfaa35ee/pandas-3.0.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9", size = 10710921, upload-time = "2026-02-17T22:19:27.181Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" },
+ { url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" },
+ { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" },
+ { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" },
+ { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" },
+ { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" },
+ { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" },
+ { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" },
+ { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" },
+ { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" },
+ { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" },
+ { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" },
+ { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" },
+ { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" },
+ { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" },
+ { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
+ { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
+ { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
+ { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
+ { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
+ { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
+ { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
+ { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
+ { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
+ { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
+ { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
+ { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
+ { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
+ { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
+ { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" },
+ { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" },
+ { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" },
+ { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" },
+ { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.9.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
+]
+
+[[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.34.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" },
+ { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" },
+ { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" },
+ { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" },
+ { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" },
+]
+
+[[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/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" },
+ { 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 = "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 = "pycryptodome"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
+ { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
+ { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
+ { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
+ { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
+ { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
+ { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydub"
+version = "0.25.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pynndescent"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "llvmlite" },
+ { name = "numba" },
+ { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "pytorch-wpe"
+version = "0.0.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.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/39/8d94737fd6fab4028687575099566a125100f3ba8c638f861506747d7b7c/pytorch_wpe-0.0.1.tar.gz", hash = "sha256:fc7e706b5411800c4483fe94db7dcd82ecf6c57bc013af529ab4fb675c9cc29c", size = 4457, upload-time = "2021-03-05T10:10:09.593Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/de/c47967a11bfe68cb28d2f19e55c7027993c3721eba79813db65d245e4ced/pytorch_wpe-0.0.1-py3-none-any.whl", hash = "sha256:fa0dc9f818fba81b36c1a51a53331cf6ed975f29b33f23e07b0deb4bee82eaad", size = 8080, upload-time = "2021-03-05T10:10:08.686Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2026.1.post1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
+]
+
+[[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" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "rapidfuzz"
+version = "3.14.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" },
+ { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" },
+ { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" },
+ { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" },
+ { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" },
+ { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" },
+ { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" },
+ { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" },
+ { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" },
+ { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" },
+ { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" },
+ { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" },
+ { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" },
+ { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" },
+ { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" },
+ { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" },
+ { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" },
+ { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" },
+ { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" },
+ { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" },
+ { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" },
+ { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" },
+ { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" },
+ { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" },
+ { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" },
+ { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" },
+ { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2026.2.28"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" },
+ { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" },
+ { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" },
+ { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" },
+ { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" },
+ { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" },
+ { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" },
+ { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" },
+ { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" },
+ { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" },
+ { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" },
+ { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" },
+ { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" },
+ { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" },
+ { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" },
+ { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" },
+ { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" },
+ { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" },
+ { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" },
+ { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" },
+ { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" },
+ { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" },
+ { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" },
+ { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" },
+ { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" },
+ { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" },
+ { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" },
+ { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" },
+ { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" },
+ { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" },
+ { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" },
+ { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+]
+
+[[package]]
+name = "s3prl"
+version = "0.4.18"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "omegaconf" },
+ { name = "protobuf" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "setuptools" },
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "torchaudio", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "torchaudio", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "tqdm" },
+ { name = "transformers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/7f/de69806654326a958d38f65190d0edb1725d221dc3a06aa2902b7fb3221f/s3prl-0.4.18.tar.gz", hash = "sha256:b59dcb12c434edc969b0b0290d4059c3a5c661adecc31db81ed9c510cd7f405e", size = 648900, upload-time = "2025-06-13T07:11:54.385Z" }
+
+[[package]]
+name = "safehttpx"
+version = "0.1.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385, upload-time = "2025-10-24T18:30:09.783Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
+ { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" },
+ { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.7.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "joblib", marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "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" },
+ { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" },
+ { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" },
+ { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" },
+ { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" },
+ { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" },
+ { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" },
+ { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" },
+ { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "joblib", marker = "python_full_version >= '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "threadpoolctl", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" },
+ { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" },
+ { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" },
+ { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" },
+ { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" },
+ { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" },
+ { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" },
+ { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" },
+ { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
+ { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" },
+ { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.15.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+]
+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" },
+ { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" },
+ { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" },
+ { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" },
+ { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" },
+ { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" },
+ { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" },
+ { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" },
+ { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+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" },
+ { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
+ { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
+ { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
+ { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
+ { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
+ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
+]
+
+[[package]]
+name = "semantic-version"
+version = "2.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
+]
+
+[[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" },
+ { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" },
+ { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" },
+ { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" },
+ { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" },
+ { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" },
+ { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" },
+ { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "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 = "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 = "soundfile"
+version = "0.13.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
+ { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
+]
+
+[[package]]
+name = "soxr"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/a7/11c36d71595b52fe84a220040ace679035953acf06b83bf2c7117c565d2c/soxr-1.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:b876a3156f67c76aef0cff1084eaf4088d9ca584bb569cb993f89a52ec5f399f", size = 206459, upload-time = "2025-09-07T13:21:46.904Z" },
+ { url = "https://files.pythonhosted.org/packages/43/5e/8962f2aeea7777d2a6e65a24a2b83c6aea1a28badeda027fd328f7f03bb7/soxr-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d3b957a7b0cc19ae6aa45d40b2181474e53a8dd00efd7bce6bcf4e60e020892", size = 164808, upload-time = "2025-09-07T13:21:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/91/00384166f110a3888ea8efd44523ba7168dd2dc39e3e43c931cc2d069fa9/soxr-1.0.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89685faedebc45af71f08f9957b61cc6143bc94ba43fe38e97067f81e272969", size = 208586, upload-time = "2025-09-07T13:21:50.341Z" },
+ { url = "https://files.pythonhosted.org/packages/75/34/e18f1003e242aabed44ed8902534814d3e64209e4d1d874f5b9b67d73cde/soxr-1.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d255741b2f0084fd02d4a2ddd77cd495be9e7e7b6f9dba1c9494f86afefac65b", size = 242310, upload-time = "2025-09-07T13:21:51.56Z" },
+ { url = "https://files.pythonhosted.org/packages/61/9c/a1c5ed106b40cc1e2e12cd58831b7f1b61c5fbdb8eceeca4b3a0b0dbef6c/soxr-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:158a4a9055958c4b95ef91dbbe280cabb00946b5423b25a9b0ce31bd9e0a271e", size = 173561, upload-time = "2025-09-07T13:21:53.03Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ce/a3262bc8733d3a4ce5f660ed88c3d97f4b12658b0909e71334cba1721dcb/soxr-1.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:28e19d74a5ef45c0d7000f3c70ec1719e89077379df2a1215058914d9603d2d8", size = 206739, upload-time = "2025-09-07T13:21:54.572Z" },
+ { url = "https://files.pythonhosted.org/packages/64/dc/e8cbd100b652697cc9865dbed08832e7e135ff533f453eb6db9e6168d153/soxr-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8dc69fc18884e53b72f6141fdf9d80997edbb4fec9dc2942edcb63abbe0d023", size = 165233, upload-time = "2025-09-07T13:21:55.887Z" },
+ { url = "https://files.pythonhosted.org/packages/75/12/4b49611c9ba5e9fe6f807d0a83352516808e8e573f8b4e712fc0c17f3363/soxr-1.0.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f15450e6f65f22f02fcd4c5a9219c873b1e583a73e232805ff160c759a6b586", size = 208867, upload-time = "2025-09-07T13:21:57.076Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/70/92146ab970a3ef8c43ac160035b1e52fde5417f89adb10572f7e788d9596/soxr-1.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f73f57452f9df37b4de7a4052789fcbd474a5b28f38bba43278ae4b489d4384", size = 242633, upload-time = "2025-09-07T13:21:58.621Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a7/628479336206959463d08260bffed87905e7ba9e3bd83ca6b405a0736e94/soxr-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f417c3d69236051cf5a1a7bad7c4bff04eb3d8fcaa24ac1cb06e26c8d48d8dc", size = 173814, upload-time = "2025-09-07T13:21:59.798Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/c7/f92b81f1a151c13afb114f57799b86da9330bec844ea5a0d3fe6a8732678/soxr-1.0.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:abecf4e39017f3fadb5e051637c272ae5778d838e5c3926a35db36a53e3a607f", size = 205508, upload-time = "2025-09-07T13:22:01.252Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1d/c945fea9d83ea1f2be9d116b3674dbaef26ed090374a77c394b31e3b083b/soxr-1.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:e973d487ee46aa8023ca00a139db6e09af053a37a032fe22f9ff0cc2e19c94b4", size = 163568, upload-time = "2025-09-07T13:22:03.558Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/80/10640970998a1d2199bef6c4d92205f36968cddaf3e4d0e9fe35ddd405bd/soxr-1.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ce273cca101aff3d8c387db5a5a41001ba76ef1837883438d3c652507a9ccc", size = 204707, upload-time = "2025-09-07T13:22:05.125Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/2726603c13c2126cb8ded9e57381b7377f4f0df6ba4408e1af5ddbfdc3dd/soxr-1.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8f2a69686f2856d37823bbb7b78c3d44904f311fe70ba49b893af11d6b6047b", size = 238032, upload-time = "2025-09-07T13:22:06.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/04/530252227f4d0721a5524a936336485dfb429bb206a66baf8e470384f4a2/soxr-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:2a3b77b115ae7c478eecdbd060ed4f61beda542dfb70639177ac263aceda42a2", size = 172070, upload-time = "2025-09-07T13:22:07.62Z" },
+ { url = "https://files.pythonhosted.org/packages/99/77/d3b3c25b4f1b1aa4a73f669355edcaee7a52179d0c50407697200a0e55b9/soxr-1.0.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:392a5c70c04eb939c9c176bd6f654dec9a0eaa9ba33d8f1024ed63cf68cdba0a", size = 209509, upload-time = "2025-09-07T13:22:08.773Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/ee/3ca73e18781bb2aff92b809f1c17c356dfb9a1870652004bd432e79afbfa/soxr-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fdc41a1027ba46777186f26a8fba7893be913383414135577522da2fcc684490", size = 167690, upload-time = "2025-09-07T13:22:10.259Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/f0/eea8b5f587a2531657dc5081d2543a5a845f271a3bea1c0fdee5cebde021/soxr-1.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:449acd1dfaf10f0ce6dfd75c7e2ef984890df94008765a6742dafb42061c1a24", size = 209541, upload-time = "2025-09-07T13:22:11.739Z" },
+ { url = "https://files.pythonhosted.org/packages/64/59/2430a48c705565eb09e78346950b586f253a11bd5313426ced3ecd9b0feb/soxr-1.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:38b35c99e408b8f440c9376a5e1dd48014857cd977c117bdaa4304865ae0edd0", size = 243025, upload-time = "2025-09-07T13:22:12.877Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/1b/f84a2570a74094e921bbad5450b2a22a85d58585916e131d9b98029c3e69/soxr-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a39b519acca2364aa726b24a6fd55acf29e4c8909102e0b858c23013c38328e5", size = 184850, upload-time = "2025-09-07T13:22:14.068Z" },
+]
+
+[[package]]
+name = "standard-aifc"
+version = "3.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "audioop-lts", marker = "python_full_version >= '3.13'" },
+ { name = "standard-chunk", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" },
+]
+
+[[package]]
+name = "standard-chunk"
+version = "3.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" },
+]
+
+[[package]]
+name = "standard-sunau"
+version = "3.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "audioop-lts", marker = "python_full_version >= '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/ae/e3707f6c1bc6f7aa0df600ba8075bfb8a19252140cd595335be60e25f9ee/standard_sunau-3.13.0-py3-none-any.whl", hash = "sha256:53af624a9529c41062f4c2fd33837f297f3baa196b0cfceffea6555654602622", size = 7364, upload-time = "2024-10-30T16:01:28.003Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.52.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
+]
+
+[[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 = "tensorboardx"
+version = "2.6.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.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging" },
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.22.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 = "tomlkit"
+version = "0.13.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "filelock", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "fsspec", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "jinja2", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "setuptools", marker = "python_full_version >= '3.12' and sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "sympy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+ { name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/d6/e6d4c57e61c2b2175d3aafbfb779926a2cfd7c32eeda7c543925dceec923/torch-2.8.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a3f16a58a9a800f589b26d47ee15aca3acf065546137fc2af039876135f4c760", size = 73611154, upload-time = "2025-08-06T14:53:10.919Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/5e/05a5c46085d9b97e928f3f037081d3d2b87fb4b4195030fc099aaec5effc/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5ae0524688fb6707c57a530c2325e13bb0090b745ba7b4a2cd6a3ce262572916", size = 73621174, upload-time = "2025-08-06T14:53:25.44Z" },
+ { url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/8b7b13bba430f5e21d77708b616f767683629fc4f8037564a177d20f90ed/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767", size = 73915128, upload-time = "2025-08-06T14:54:34.769Z" },
+ { url = "https://files.pythonhosted.org/packages/04/6e/650bb7f28f771af0cb791b02348db8b7f5f64f40f6829ee82aa6ce99aabe/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211", size = 73632395, upload-time = "2025-08-06T14:55:28.645Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.8.0+cu128"
+source = { registry = "https://download.pytorch.org/whl/cu128" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+]
+dependencies = [
+ { name = "filelock", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "fsspec", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "jinja2", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
+ { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
+ { name = "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-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools", marker = "(python_full_version >= '3.12' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform == 'win32')" },
+ { name = "sympy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+wheels = [
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0c96999d15cf1f13dd7c913e0b21a9a355538e6cfc10861a17158320292f5954" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:43938e9a174c90e5eb9e906532b2f1e21532bbfa5a61b65193b4f54714d34f9e" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:039b9dcdd6bdbaa10a8a5cd6be22c4cb3e3589a341e5f904cbb571ca28f55bed" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:34c55443aafd31046a7963b63d30bc3b628ee4a704f826796c865fdfd05bb596" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4354fc05bb79b208d6995a04ca1ceef6a9547b1c4334435574353d381c55087c" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:0ad925202387f4e7314302a1b4f8860fa824357f9b1466d7992bf276370ebcff" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3a852369a38dec343d45ecd0bc3660f79b88a23e0c878d18707f7c13bf49538f" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:9e20646802b7fc295c1f8b45fefcfc9fb2e4ec9cbe8593443cd2b9cc307c8405" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4295a22d69408e93d25f51e8d5d579345b6b802383e9414b0f3853ed433d53ae" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:970b4f4661fa7b44f6a7e6df65de7fc4a6fff2af610dc415c1d695ca5f1f37d2" },
+]
+
+[[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.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/17cb15a383cf2135330371e034d13b9043dc6d8bd07c871b5aa3064fbed1/torch_complex-0.4.4.tar.gz", hash = "sha256:4153fd6b24a0bad689e6f193bfbd00f38283b1890d808bef684ddc6d1f63fd3f", size = 10025, upload-time = "2024-06-28T07:10:28.136Z" }
+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.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.11.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.11' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+dependencies = [
+ { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/81/92d34ff136b17ddda872f6d8149f2ca927ad53a37ae26d02cb5f66435772/torchaudio-2.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c2f44cf279f673cfcdd8f576c349eee8bedf8caab351a5dd78b32970cc34a212", size = 1852315, upload-time = "2025-08-06T14:58:32.64Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/bf/6b01ef3defb8d0a772c863588711e9b2b011c27d6b37c1b9d15a359c8442/torchaudio-2.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9276857d241c6de257af765c0f51fc011af38cb725401495121b280913007cf", size = 1859094, upload-time = "2025-08-06T14:58:35.078Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/cc/c2e2a3eb6ee956f73c68541e439916f8146170ea9cc61e72adea5c995312/torchaudio-2.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ddef94bf181e6447cbb05f38beaca8f6c5bb8d2b9ddced1aa3452025b9fc70d3", size = 1856736, upload-time = "2025-08-06T14:58:36.3Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ea/2a68259c4dbb5fe44ebfdcfa40b115010d8c677221a7ef0f5577f3c4f5f1/torchaudio-2.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f851d32e94ca05e470f0c60e25726ec1e0eb71cb2ca5a0206b7fd03272ccc3c8", size = 1857045, upload-time = "2025-08-06T14:58:51.984Z" },
+ { url = "https://files.pythonhosted.org/packages/34/1a/69b9f8349d9d57953d5e7e445075cbf74000173fb5f5d5d9e9d59415fc63/torchaudio-2.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:68df9c9068984edff8065c2b6656725e6114fe89281b0cf122c7505305fc98a4", size = 1935600, upload-time = "2025-08-06T14:58:46.051Z" },
+]
+
+[[package]]
+name = "torchaudio"
+version = "2.8.0+cu128"
+source = { registry = "https://download.pytorch.org/whl/cu128" }
+resolution-markers = [
+ "(python_full_version >= '3.14' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform == 'win32')",
+ "(python_full_version == '3.13.*' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.12.*' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform == 'win32')",
+ "(python_full_version == '3.11.*' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform == 'win32')",
+ "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')",
+]
+dependencies = [
+ { name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+wheels = [
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a0161e95285a0b716de210fee0392151d601e7da3cc86595008d826abff48a8c" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:5d7a9d913e2744573ed3b7ec2f781ed39833c81c9c41859973ec10ac174c2366" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f4409df567d0723a7a3a89d32c7552a17e0ff6f137ea26a0d268c665259b2995" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:7a1eb6154e05b8056b34c7a41495e09d57f79eb0180eb4e7f3bb2a61845ca8ea" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:145b8a0c21cfcaa1705c67173c5d439087e0e120d5da9bc344746f937901d243" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:cce3a60cd9a97f7360c8f95504ac349311fb7d6b9b826135936764f4de5f782d" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:410bb8ea46225efe658e5d27a3802c181a2255913003621a5d25a51aca8018d9" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:3146bbd48992d215f6bb1aef9626d734c3180b377791ded2a4d4d2c0e63c0cc2" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:04b410f93337fc6c16576d0c88e2a31091aef9d1fd212ebb8cd26899dba175e0" },
+ { url = "https://download-r2.pytorch.org/whl/cu128/torchaudio-2.8.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:1054e0a7613cac54ed9b3784a5fcbe023748a70004d9cca74c5f9ae00a1fdfd1" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
+]
+
+[[package]]
+name = "transformers"
+version = "5.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/ee/0ee5f64a87eeda19bbad9bc54ae5ca5b98186ed00055281fd40fb4beb10e/triton-3.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ff2785de9bc02f500e085420273bb5cc9c9bb767584a4aa28d6e360cec70128", size = 155430069, upload-time = "2025-07-30T19:58:21.715Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/39/43325b3b651d50187e591eefa22e236b2981afcebaefd4f2fc0ea99df191/triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b70f5e6a41e52e48cfc087436c8a28c17ff98db369447bcaff3b887a3ab4467", size = 155531138, upload-time = "2025-07-30T19:58:29.908Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" },
+ { url = "https://files.pythonhosted.org/packages/30/7b/0a685684ed5322d2af0bddefed7906674f67974aa88b0fae6e82e3b766f6/triton-3.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00be2964616f4c619193cb0d1b29a99bd4b001d7dc333816073f92cf2a8ccdeb", size = 155569223, upload-time = "2025-07-30T19:58:44.017Z" },
+ { url = "https://files.pythonhosted.org/packages/20/63/8cb444ad5cdb25d999b7d647abac25af0ee37d292afc009940c05b82dda0/triton-3.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7936b18a3499ed62059414d7df563e6c163c5e16c3773678a3ee3d417865035d", size = 155659780, upload-time = "2025-07-30T19:58:51.171Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
+]
+
+[[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 = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "umap-learn"
+version = "0.5.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numba" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pynndescent" },
+ { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/9a/a1e4a257a9aa979dac4f6d5781dac929cbb0949959e2003ed82657d10b0f/umap_learn-0.5.11.tar.gz", hash = "sha256:31566ffd495fbf05d7ab3efcba703861c0f5e6fc6998a838d0e2becdd00e54f5", size = 96409, upload-time = "2026-01-12T20:44:47.553Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/d2/fcf7192dd1cd8c090b6cfd53fa223c4fb2887a17c47e06bc356d44f40dfb/umap_learn-0.5.11-py3-none-any.whl", hash = "sha256:cb17adbde9d544ba79481b3ab4d81ac222e940f3d9219307bea6044f869af3cc", size = 90890, upload-time = "2026-01-12T20:44:46.511Z" },
+]
+
+[[package]]
+name = "unidecode"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.42.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" },
+]
+
+[[package]]
+name = "webdataset"
+version = "1.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "braceexpand" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/3a/68800d92e065cf4750ebecf973b13979c0c929b439e1293012938862038d/webdataset-1.0.2.tar.gz", hash = "sha256:7f0498be827cfa46cc5430a58768a24e2c6a410676a61be1838f53d61afdaab4", size = 80090, upload-time = "2025-06-19T23:26:21.945Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/00/aca6beb3658dab4ed3dbb41a78e6e7f31342e0b41d28088f205525751601/webdataset-1.0.2-py3-none-any.whl", hash = "sha256:3dbfced32b25c0d199c6b9787937b6f85742bc3c84f652c846893075c1c082d9", size = 74956, upload-time = "2025-06-19T23:26:20.354Z" },
+]
+
+[[package]]
+name = "zhconv"
+version = "1.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/25/47/c8ae2d5d4025e253211ff3d8c163f457db1da94976cb582337a5ab76cb87/zhconv-1.4.3.tar.gz", hash = "sha256:ad42d9057ca0605f8e41d62b67ca797f879f58193ee6840562c51459b2698c45", size = 211571, upload-time = "2021-10-31T05:59:44.096Z" }
+
+[[package]]
+name = "zhon"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/8d/e6c8ee4df75940ddecb6f5e7b2753a9a3914e6cd357e7238987661723d81/zhon-2.1.1.tar.gz", hash = "sha256:c8da424fad4aa698ddd3e1735515b49ff522c84dd1091e8fafd986ac7c6898f0", size = 84448, upload-time = "2024-11-20T00:29:10.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/13/b8d6ca3a41bf5959bc3915678837385bf668162a7295140d81ccae0b96b7/zhon-2.1.1-py3-none-any.whl", hash = "sha256:9ec0d42229ae60708a045270f755ccefb48f2689f2a6ce0bc6597abf6c4a9871", size = 83933, upload-time = "2024-11-20T00:29:08.209Z" },
+]