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 +

+ +

+ Hugging Face Model +   + Hugging Face Space +   + +   + +

+ +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 | +| ------------ | ----------------------- | +|![wechat](https://k2-fsa.org/zh-CN/assets/pic/wechat_group.jpg) |![wechat](https://k2-fsa.org/zh-CN/assets/pic/wechat_account.jpg) | + +--- + +## 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

+
+
+ +
+ + + +
+ + {/* ═══ DUB TAB ═══ */} + {mode === 'dub' ? ( +
+ {dubStep === 'idle' && ( +
+
Upload Video for Dubbing
+ + setDubVideoFile(e.target.files[0])} style={{display:'none'}} id="video-upload"/> + +
+ )} + + {(dubStep === 'uploading' || dubStep === 'transcribing') && ( +
+ +

{dubStep === 'uploading' ? 'Extracting audio...' : 'Transcribing with Whisper...'}

+
+ )} + + {(dubStep === 'editing' || dubStep === 'generating' || dubStep === 'done') && ( +
+
+
+
+ {dubFilename} ({formatTime(dubDuration)}) — {dubSegments.length} segments +
+ +
+ + {/* Full Transcript */} + {dubTranscript && ( +
+
setShowTranscript(!showTranscript)} style={{marginTop:0}}> + Full Transcript + {showTranscript ? : } +
+ {showTranscript && ( +
+ {dubTranscript} +
+ )} +
+ )} + +
+
+
Target Language
+ +
+
+
ISO Code (YouTube)
+ +
+
+
Voice Style
+ setDubInstruct(e.target.value)}/> +
+
+ + {/* Translate All button */} +
+ +
+ +
+
+ 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'}/> + +
+ {dubStep === 'done' && ( + + )} + +
+
+ ))} +
+
+ +
+ {dubStep === 'generating' && ( +
+
+ Dubbing {dubProgress.current}/{dubProgress.total} + {dubProgress.text} +
+
+
+ )} + {dubStep === 'done' && ( +
+ + Done! Tracks: {dubTracks.join(', ')} +
+ )} + {dubError && ( +
+ {dubError} +
+ )} +
+ + + + +
+ {dubStep === 'done' && ( +
+
+ setPreserveBg(e.target.checked)} style={{cursor: 'pointer'}} /> + +
+
+ setMakeDefaultTrack(e.target.checked)} style={{cursor: 'pointer'}} /> + +
+
+ )} +
+
+ )} +
+ ) : ( + <> + {/* ═══ CLONE / DESIGN ═══ */} +
+
Prompt
+ {mode === 'design' && ( +
+ {PRESETS.map(p => )} +
+ )} +