From beb161832111b56087e4426ba14e1a5142b8c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=96=E5=85=83?= Date: Fri, 31 Jul 2026 16:41:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=AE=9A=E5=88=B6:=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E4=B8=AD=E6=96=87=E6=96=87=E6=9C=AC/CLI=20ba?= =?UTF-8?q?ckend/.gitignore=20=E4=BF=AE=E5=A4=8D=E5=A4=A7=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=BF=BD=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- app.py | 208 +++++++++++++++++++++- docs/PRD.md | 492 ++++++++++++++++++++++++++++++++++++++++++++++++++++ test_tts.py | 61 +++++++ uv.lock | 129 ++++++++++++-- 5 files changed, 867 insertions(+), 27 deletions(-) create mode 100644 docs/PRD.md create mode 100644 test_tts.py diff --git a/.gitignore b/.gitignore index f7fa9812..8f8ad6d4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ launch.json __pycache__ voxcpm.egg-info .DS_Store -./pretrained_models/ +pretrained_models/ +*.wav +*.gguf app_local.py diff --git a/app.py b/app.py index 15448281..ee358a4f 100644 --- a/app.py +++ b/app.py @@ -159,9 +159,7 @@ I18N = gr.I18n(**_I18N_TRANSLATIONS) -DEFAULT_TARGET_TEXT = ( - "VoxCPM2 is a creative multilingual TTS model from ModelBest, " "designed to generate highly realistic speech." -) +DEFAULT_TARGET_TEXT = "轻轻地你走了,不带走一片云彩。" _CUSTOM_CSS = """ .logo-container { @@ -225,10 +223,21 @@ class VoxCPMDemo: - def __init__(self, model_id: str = "openbmb/VoxCPM2", device: str = "auto") -> None: + def __init__( + self, + model_id: str = "openbmb/VoxCPM2", + device: str = "auto", + load_denoiser: bool = True, + backend: str = "python", + cli_bin_path: Optional[str] = None, + cli_base_lm: Optional[str] = None, + cli_acoustic: Optional[str] = None, + cli_timesteps: int = 7, + ) -> None: self.device = resolve_runtime_device(device, "cuda") logger.info(f"Running VoxCPM on device: {self.device}") self.optimize = self.device.startswith("cuda") + self.load_denoiser = load_denoiser self.asr_model_id = "iic/SenseVoiceSmall" self.asr_device = "cuda:0" if self.device.startswith("cuda") else "cpu" @@ -237,12 +246,29 @@ def __init__(self, model_id: str = "openbmb/VoxCPM2", device: str = "auto") -> N self.voxcpm_model: Optional[voxcpm.VoxCPM] = None self._model_id = model_id + # --- CLI backend (llama.cpp-omni) --- + self.backend = backend + self.cli_bin_path = cli_bin_path + self.cli_base_lm = cli_base_lm + self.cli_acoustic = cli_acoustic + self.cli_timesteps = cli_timesteps + if self.backend == "cli": + for label, path in [("cli-bin", self.cli_bin_path), ("cli-base-lm", self.cli_base_lm), ("cli-acoustic", self.cli_acoustic)]: + if not path or not os.path.isfile(path): + raise FileNotFoundError(f"CLI backend requires --{label}, file not found: {path}") + logger.info(f"Backend: CLI (llama.cpp-omni) | timesteps={self.cli_timesteps}") + logger.info(f" bin: {self.cli_bin_path}") + logger.info(f" base_lm: {self.cli_base_lm}") + logger.info(f" acoustic: {self.cli_acoustic}") + logger.info(" NOTE: CLI mode does not support ASR auto-transcription; please type the transcript manually.") + def get_or_load_voxcpm(self) -> voxcpm.VoxCPM: if self.voxcpm_model is not None: return self.voxcpm_model logger.info(f"Loading model: {self._model_id}") self.voxcpm_model = voxcpm.VoxCPM.from_pretrained( self._model_id, + load_denoiser=self.load_denoiser, optimize=self.optimize, device=self.device, ) @@ -298,6 +324,92 @@ def _build_generate_kwargs( generate_kwargs["prompt_text"] = prompt_text_clean return generate_kwargs + def _generate_via_cli( + self, + final_text: str, + audio_path: Optional[str], + prompt_text_clean: Optional[str], + cfg_value: float, + timesteps: int, + seed: Optional[int], + ) -> Tuple[int, np.ndarray, Optional[int]]: + import subprocess + import tempfile + import soundfile as sf + import librosa + + tmp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) + tmp_wav.close() + out_wav = tmp_wav.name + + # Convert reference audio to standard PCM WAV if needed (CLI only supports WAV) + cli_audio_path = audio_path + tmp_ref_wav = None + if audio_path: + try: + ref_data, ref_sr = librosa.load(audio_path, sr=16000, mono=True) + tmp_ref = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) + tmp_ref.close() + sf.write(tmp_ref.name, ref_data, ref_sr, subtype="PCM_16") + cli_audio_path = tmp_ref.name + tmp_ref_wav = tmp_ref.name + logger.info(f"[CLI] Converted reference audio to 16kHz WAV: {cli_audio_path}") + except Exception as e: + logger.warning(f"[CLI] Failed to convert reference audio: {e}, using original path") + cli_audio_path = audio_path + + cmd = [ + self.cli_bin_path, + "-t", final_text, + "-o", out_wav, + "--timesteps", str(timesteps), + "--cfg", str(cfg_value), + "--seed", str(seed if seed is not None else 42), + ] + + if cli_audio_path and prompt_text_clean and prompt_text_clean.strip(): + # Ultimate cloning: use prompt-wav + prompt-text + reference + cmd += ["--prompt-wav", cli_audio_path, "--prompt-text", prompt_text_clean, "-r", cli_audio_path] + logger.info(f"[CLI] Mode: ultimate cloning (prompt_text length={len(prompt_text_clean)})") + elif cli_audio_path: + # Controllable cloning: reference only + cmd += ["-r", cli_audio_path] + logger.info("[CLI] Mode: controllable cloning (reference only)") + + cmd += [self.cli_base_lm, self.cli_acoustic] + + logger.info(f"[CLI] Running voxcpm2-cli with timesteps={timesteps}") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) + if result.returncode != 0: + stderr_tail = result.stderr[-500:] if result.stderr else "(no stderr)" + # If ultimate cloning failed, retry with controllable cloning (reference only) + if "--prompt-wav" in cmd and result.returncode < 0: + logger.warning(f"[CLI] Ultimate cloning crashed (code {result.returncode}), retrying with controllable cloning...") + retry_cmd = [c for c in cmd if c not in ("--prompt-wav", cli_audio_path, "--prompt-text", prompt_text_clean)] + # Remove the first cli_audio_path that was -r's argument, keep the second one + retry_cmd = [self.cli_bin_path, "-t", final_text, "-o", out_wav, + "--timesteps", str(timesteps), "--cfg", str(cfg_value), + "--seed", str(seed if seed is not None else 42), + "-r", cli_audio_path, + self.cli_base_lm, self.cli_acoustic] + result = subprocess.run(retry_cmd, capture_output=True, text=True, timeout=600) + if result.returncode != 0: + stderr_tail = result.stderr[-500:] if result.stderr else "(no stderr)" + raise RuntimeError(f"voxcpm2-cli retry also failed (code {result.returncode}): {stderr_tail}") + else: + raise RuntimeError(f"voxcpm2-cli exited with code {result.returncode}: {stderr_tail}") + wav, sr = sf.read(out_wav) + logger.info(f"[CLI] Generated: {len(wav)/sr:.2f}s audio, sample_rate={sr}") + return (sr, wav, seed) + finally: + for f in [out_wav, tmp_ref_wav]: + if f: + try: + os.unlink(f) + except OSError: + pass + def generate_tts_audio( self, text_input: str, @@ -310,21 +422,29 @@ def generate_tts_audio( inference_timesteps: int = 10, seed: Optional[int] = None, ) -> Tuple[int, np.ndarray, Optional[int]]: - current_model = self.get_or_load_voxcpm() - text = (text_input or "").strip() if len(text) == 0: raise ValueError("Please input text to synthesize.") control = (control_instruction or "").strip() - # Strip any parentheses (half-width/full-width) from control text to avoid - # breaking the "(control)text" prompt format expected by the model. control = re.sub(r"[()()]", "", control).strip() final_text = f"({control}){text}" if control else text audio_path = reference_wav_path_input if reference_wav_path_input else None prompt_text_clean = (prompt_text or "").strip() or None + if self.backend == "cli": + return self._generate_via_cli( + final_text=final_text, + audio_path=audio_path, + prompt_text_clean=prompt_text_clean, + cfg_value=cfg_value_input, + timesteps=self.cli_timesteps, + seed=seed, + ) + + current_model = self.get_or_load_voxcpm() + if audio_path and prompt_text_clean: logger.info(f"[Voice Cloning] prompt_wav + prompt_text + reference_wav") elif audio_path: @@ -429,6 +549,18 @@ def _run_asr_if_needed(checked, audio_path): gr.Markdown(I18N("usage_instructions")) + if demo.backend == "cli": + gr.Markdown( + f"> **Backend: CLI (llama.cpp-omni)** — timesteps={demo.cli_timesteps} | " + f"Fast Metal acceleration. Note: ASR auto-transcription is unavailable; " + f"please type the reference audio transcript manually for ultimate cloning." + ) + else: + gr.Markdown( + f"> **Backend: Python (PyTorch)** — Full features including ASR and denoiser. " + f"Slower on Apple Silicon (MPS uses float32)." + ) + with gr.Row(): with gr.Column(): reference_wav = gr.Audio( @@ -560,8 +692,23 @@ def run_demo( show_error: bool = True, model_id: str = "openbmb/VoxCPM2", device: str = "auto", + load_denoiser: bool = True, + backend: str = "python", + cli_bin_path: Optional[str] = None, + cli_base_lm: Optional[str] = None, + cli_acoustic: Optional[str] = None, + cli_timesteps: int = 7, ): - demo = VoxCPMDemo(model_id=model_id, device=device) + demo = VoxCPMDemo( + model_id=model_id, + device=device, + load_denoiser=load_denoiser, + backend=backend, + cli_bin_path=cli_bin_path, + cli_base_lm=cli_base_lm, + cli_acoustic=cli_acoustic, + cli_timesteps=cli_timesteps, + ) interface = create_demo_interface(demo) interface.queue(max_size=10, default_concurrency_limit=1).launch( server_name=server_name, @@ -597,10 +744,53 @@ def run_demo( default="auto", help="Runtime device: auto, cpu, mps, cuda, or cuda:N (default: auto)", ) + parser.add_argument( + "--no-denoiser", + action="store_true", + default=False, + help="Disable the denoiser (zipenhancer) to avoid remote download", + ) + parser.add_argument( + "--backend", + type=str, + default="python", + choices=["python", "cli"], + help="Generation backend: 'python' (full features, slower on Mac) or 'cli' (llama.cpp-omni, 15-20x faster on Apple Silicon)", + ) + parser.add_argument( + "--cli-bin", + type=str, + default=None, + help="Path to voxcpm2-cli binary (required for --backend cli)", + ) + parser.add_argument( + "--cli-base-lm", + type=str, + default=None, + help="Path to VoxCPM2-BaseLM GGUF file (required for --backend cli)", + ) + parser.add_argument( + "--cli-acoustic", + type=str, + default=None, + help="Path to VoxCPM2-Acoustic GGUF file (required for --backend cli)", + ) + parser.add_argument( + "--cli-timesteps", + type=int, + default=7, + help="CFM inference timesteps for CLI backend (default: 7, lower = faster)", + ) args = parser.parse_args() run_demo( model_id=args.model_id, server_name=args.host, server_port=args.port, device=args.device, + load_denoiser=not args.no_denoiser, + backend=args.backend, + cli_bin_path=args.cli_bin, + cli_base_lm=args.cli_base_lm, + cli_acoustic=args.cli_acoustic, + cli_timesteps=args.cli_timesteps, ) diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 00000000..b2970c15 --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,492 @@ +# VoxCPM 产品需求文档 (PRD) + +> **版本**: 1.0 | **日期**: 2026-07-20 | **状态**: 已实现 + +--- + +## 1. 产品概述 + +### 1.1 产品定位 + +VoxCPM 是一个**无离散音频分词器(Tokenizer-Free)**的文本转语音(TTS)系统,通过端到端的**扩散自回归架构**直接生成连续语音表征。最新版本 **VoxCPM2** 拥有 20 亿参数,在超过 200 万小时的多语言语音数据上训练,支持 30 种语言 + 9 种中文方言,原生输出 48kHz 高质量音频。 + +### 1.2 核心价值 + +| 特性 | 描述 | +|------|------| +| **多语言** | 30 种全球语言 + 9 种中国方言,无需语言标签 | +| **音色设计** | 用自然语言描述凭空创造全新音色 | +| **声音克隆** | 从短音频克隆任意声音,可叠加风格控制 | +| **极致克隆** | 音频续写还原声音细节特征 | +| **48kHz 高品质** | 非对称编解码设计,内置超分能力 | +| **完全开源** | Apache-2.0 协议,免费商用 | + +### 1.3 目标用户 + +- 需要高质量语音合成的开发者 +- 需要声音克隆和定制音色的内容创作者 +- 需要多语言/方言语音的出海业务 +- 需要端侧部署的设备厂商 + +--- + +## 2. 模型版本对比 + +| | **VoxCPM2** | **VoxCPM1.5** | **VoxCPM-0.5B** | +|---|:---:|:---:|:---:| +| 状态 | 最新版本 | 稳定版 | 旧版 | +| 参数量 | 2B | 0.6B | 0.5B | +| 输出采样率 | 48kHz | 44.1kHz | 16kHz | +| 语言数 | 30 + 9 方言 | 2 (中/英) | 2 (中/英) | +| 音色设计 | 支持 | 不支持 | 不支持 | +| 可控克隆 | 支持 | 不支持 | 不支持 | +| 参考音频克隆(无需文本) | 支持 | 不支持 | 不支持 | +| 音频续写克隆 | 支持 | 支持 | 支持 | +| SFT / LoRA 微调 | 支持 | 支持 | 支持 | +| RTF (RTX 4090) | ~0.30 | ~0.15 | ~0.17 | + +--- + +## 3. 功能模块 + +### 3.1 音色设计 (Voice Design) + +**目标**: 无需参考音频,用自然语言描述创造全新音色。 + +**使用方式**: 在文本开头用括号写入音色描述: + +``` +(控制指令描述)目标合成文本 +``` + +**支持的描述维度**: + +| 维度 | 示例 | +|------|------| +| 性别 | 男性、女性 | +| 年龄 | 年轻、中老年、儿童 | +| 音色 | 温柔、低沉、沙哑、甜美、阴冷 | +| 情绪 | 暴躁、忧郁、兴奋、欢快、平静 | +| 语速 | 快速、缓慢、适中 | +| 方言 | 粤语、河南话、四川话 | + +**示例**: + +``` +(年轻女性,声音温柔甜美)你好,欢迎使用VoxCPM2! +(中老年女性,声音低沉阴冷,语速缓慢而有力)哀家在这深宫待了四十年。 +(Relaxed young male voice, slightly nasal, lazy drawl)Dude, did you see that set? +``` + +### 3.2 声音克隆 (Voice Cloning) + +#### 3.2.1 参考音频克隆 (Controllable Cloning) + +**目标**: 上传参考音频克隆音色,可叠加风格控制指令。 + +- **无需**提供参考音频的文本转录 +- 通过 `ref_audio` tokens 结构化隔离参考音色 +- 可叠加控制指令调节情绪/语速/风格 + +```python +wav = model.generate( + text="这是VoxCPM2生成的克隆语音。", + reference_wav_path="path/to/voice.wav", +) +``` + +#### 3.2.2 可控克隆 (Controllable Cloning with Style) + +**目标**: 克隆音色同时用控制指令调节风格。 + +```python +wav = model.generate( + text="(稍快一点,欢快的语气)这是带风格控制的克隆语音。", + reference_wav_path="path/to/voice.wav", +) +``` + +#### 3.2.3 极致克隆 (Ultimate Cloning) + +**目标**: 提供参考音频及其精确文本转录,通过音频续写实现最高保真度。 + +- 模型将参考音频视为已说出的前文,进行**音频续写** +- 控制指令与此模式**互斥** +- 为获得最高相似度,可将同一音频同时传给 `reference_wav_path` 和 `prompt_wav_path` + +```python +wav = model.generate( + text="这是极致克隆演示。", + prompt_wav_path="path/to/voice.wav", + prompt_text="参考音频的文本转录。", + reference_wav_path="path/to/voice.wav", +) +``` + +#### 三种克隆模式对比 + +| 模式 | 需要参考音频 | 需要转录文本 | 支持控制指令 | 克隆保真度 | 版本支持 | +|------|:---:|:---:|:---:|:---:|:---:| +| 参考音频克隆 | 是 | 否 | 是 | 中 | 仅 VoxCPM2 | +| 可控克隆 | 是 | 否 | 是 | 中 | 仅 VoxCPM2 | +| 极致克隆 | 是 | 是 | 否 | 高 | 全部版本 | + +### 3.3 流式生成 (Streaming) + +**目标**: 边生成边输出,降低首包延迟。 + +```python +import numpy as np + +chunks = [] +for chunk in model.generate_streaming(text="流式语音合成"): + chunks.append(chunk) +wav = np.concatenate(chunks) +``` + +**性能指标**: + +| 平台 | RTF (标准) | RTF (Nano-vLLM) | +|------|:---:|:---:| +| RTX 4090 | ~0.30 | ~0.13 | + +### 3.4 微调 (Fine-tuning) + +#### 3.4.1 全参数微调 (SFT) + +通过 `conf/voxcpm_v2/voxcpm_finetune_all.yaml` 配置: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| learning_rate | 1e-5 | 学习率 | +| batch_size | 2 | 批次大小 (grad_accum=8) | +| num_iters | - | 迭代次数 | +| max_steps | - | 最大步数 | +| warmup_steps | - | 预热步数 | + +#### 3.4.2 LoRA 微调 + +通过 `conf/voxcpm_v2/voxcpm_finetune_lora.yaml` 配置: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| r | 32 | LoRA rank | +| alpha | 32 | LoRA alpha | +| dropout | 0.0 | LoRA dropout | +| enable_lm | true | 对 LM 层应用 LoRA | +| enable_dit | true | 对 DiT 层应用 LoRA | +| enable_proj | false | 对投影层应用 LoRA | + +#### 3.4.3 训练数据格式 + +JSONL 格式,每行一个 JSON 对象: + +```json +{"audio": "path/to/audio.wav", "text": "对应的文本转录"} +{"audio": "path/to/audio2.wav", "text": "带时长标注", "duration": 3.5} +``` + +#### 3.4.4 LoRA 微调 WebUI + +端口 7860,包含两个标签页: + +- **训练标签页**: 配置训练参数、启动/停止训练、实时日志 +- **推理标签页**: 加载 LoRA 检查点、热切换不同 LoRA 模型、合成语音 + +### 3.5 时间戳对齐 (Timestamps) + +**目标**: 为生成的音频提供文本-时间对齐信息。 + +| 粒度 | 说明 | +|------|------| +| `segment` | 段级时间戳 | +| `word` | 词级时间戳(默认) | +| `char` | 字符级时间戳(best-effort) | + +**安装**: `pip install "voxcpm[timestamps]"` + +**CLI 使用**: +```bash +voxcpm design --text "欢迎使用VoxCPM2。" --output out.wav \ + --timestamps --timestamp-level word --timestamp-language zh +``` + +**输出格式**: JSON 文件,包含每个文本片段的开始/结束时间。 + +### 3.6 文本规范化 + +基于 `wetext` 库,规范化数字、日期、缩写等: + +```bash +voxcpm design --text "今天是2024年3月15日" --output out.wav --normalize +``` + +### 3.7 音频降噪 + +基于 ZipEnhancer (`iic/speech_zipenhancer_ans_multiloss_16k_base`),对参考/提示音频进行降噪增强: + +```python +wav = model.generate( + text="...", + reference_wav_path="noisy_ref.wav", + denoise=True, +) +``` + +--- + +## 4. 接口规格 + +### 4.1 Python API + +#### 4.1.1 模型加载 + +```python +from voxcpm import VoxCPM + +model = VoxCPM.from_pretrained( + hf_model_id="openbmb/VoxCPM2", # 或本地路径 + load_denoiser=True, # 是否加载降噪器 + optimize=True, # 是否 torch.compile 优化 + device="auto", # auto/cpu/mps/cuda + lora_config=None, # LoRA 配置 + lora_weights_path=None, # LoRA 权重路径 +) +``` + +#### 4.1.2 生成参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `text` | str | (必填) | 目标文本 | +| `prompt_wav_path` | str | None | 提示音频路径(续写模式) | +| `prompt_text` | str | None | 提示音频转录文本 | +| `reference_wav_path` | str | None | 参考音频路径(克隆模式) | +| `cfg_value` | float | 2.0 | CFG 引导尺度 | +| `inference_timesteps` | int | 10 | 扩散推理步数 | +| `min_len` | int | 2 | 最小音频长度 | +| `max_len` | int | 4096 | 最大 token 长度 | +| `normalize` | bool | False | 文本规范化 | +| `denoise` | bool | False | 音频降噪 | +| `seed` | int | None | 随机种子 | + +#### 4.1.3 LoRA 管理 + +| 方法 | 说明 | +|------|------| +| `load_lora(path)` | 加载 LoRA 权重 | +| `unload_lora()` | 卸载 LoRA 权重 | +| `set_lora_enabled(bool)` | 启用/禁用 LoRA | +| `get_lora_state_dict()` | 获取 LoRA 参数 | +| `lora_enabled` | 检查 LoRA 是否已配置 | + +### 4.2 CLI 工具 + +#### 4.2.1 子命令 + +| 命令 | 用途 | 必填参数 | +|------|------|---------| +| `voxcpm design` | 音色设计合成 | `--text`, `--output` | +| `voxcpm clone` | 声音克隆 | `--text`, `--output`, `--reference-audio` | +| `voxcpm batch` | 批量合成 | `--input`, `--output-dir` | +| `voxcpm validate` | 验证训练数据 | `--manifest` | + +#### 4.2.2 通用参数 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `--text` / `-t` | - | 目标文本 | +| `--control` | None | 控制指令 | +| `--cfg-value` | 2.0 | CFG 引导尺度 (0.1-10.0) | +| `--inference-timesteps` | 10 | 推理步数 (1-100) | +| `--normalize` | False | 文本规范化 | +| `--seed` | None | 随机种子 | +| `--device` | auto | 运行设备 | +| `--model-path` | None | 本地模型路径 | +| `--no-denoiser` | False | 禁用降噪器 | +| `--no-optimize` | False | 禁用优化 | + +#### 4.2.3 克隆专用参数 + +| 参数 | 说明 | +|------|------| +| `--reference-audio` / `-ra` | 参考音频路径 | +| `--prompt-audio` / `-pa` | 提示音频路径 | +| `--prompt-text` / `-pt` | 提示音频转录文本 | +| `--denoise` | 降噪参考/提示音频 | + +### 4.3 Web Demo (app.py) + +#### 4.3.1 后端模式 + +| 后端 | 参数 | 特点 | +|------|------|------| +| **Python** | `--backend python` | 完整功能,含 ASR 和降噪;MPS 上慢 | +| **CLI** | `--backend cli` | Metal 加速,快 15-20x;不支持 ASR | + +#### 4.3.2 启动命令 + +**Python 后端**: +```bash +python app.py --model-id ./pretrained_models/VoxCPM2 --device mps --no-denoiser --port 8808 +``` + +**CLI 后端**: +```bash +python app.py \ + --backend cli \ + --cli-bin /path/to/voxcpm2-cli \ + --cli-base-lm /path/to/VoxCPM2-BaseLM-Q8_0.gguf \ + --cli-acoustic /path/to/VoxCPM2-Acoustic-F16.gguf \ + --cli-timesteps 7 \ + --port 8808 +``` + +#### 4.3.3 UI 组件 + +| 组件 | 说明 | +|------|------| +| 参考音频上传 | 支持上传/麦克风录制 | +| 极致克隆开关 | 开启后显示转录文本输入框 | +| 转录文本 | 参考音频文本转录(Python 模式自动 ASR) | +| 控制指令 | 音色描述文本 | +| 目标文本 | 要合成的文本 | +| 高级设置 | CFG、步数、种子、降噪、文本规范化 | +| 生成按钮 | 触发语音合成 | +| 音频输出 | 播放和下载生成结果 | + +#### 4.3.4 高级设置 + +| 设置 | 范围 | 默认值 | +|------|------|--------| +| CFG (引导尺度) | 1.0 - 3.0 | 2.0 | +| LocDiT 步数 | 1 - 50 | 10 | +| 种子 | 0 - 2^32 | 随机 | +| 参考音频降噪 | 开/关 | 关 | +| 文本规范化 | 开/关 | 关 | + +### 4.4 llama.cpp-omni CLI + +#### 4.4.1 命令格式 + +```bash +voxcpm2-cli [options] +``` + +#### 4.4.2 参数 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `-t` / `--text` | - | 目标文本 | +| `-o` / `--output` | output.wav | 输出文件 | +| `-r` / `--reference` | - | 参考音频(克隆模式) | +| `--prompt-wav` | - | 提示音频(极致克隆) | +| `--prompt-text` | - | 提示音频转录文本 | +| `--timesteps` | 10 | CFM 推理步数 | +| `--cfg` | 2.0 | CFG 引导尺度 | +| `--seed` | 42 | 随机种子 | +| `--steps` | 200 | 最大解码步数 | +| `--cpu` | False | 使用 CPU 后端 | + +#### 4.4.3 GGUF 权重 + +| 文件 | 量化 | 大小 | 说明 | +|------|------|------|------| +| VoxCPM2-BaseLM-F16.gguf | F16 | 3.0 GB | 全精度 LM | +| VoxCPM2-BaseLM-Q8_0.gguf | Q8_0 | 1.6 GB | 量化 LM(推荐) | +| VoxCPM2-Acoustic-F16.gguf | F16 | 1.7 GB | 声学模型 | + +--- + +## 5. 多语言支持 + +### 5.1 支持的 30 种语言 + +阿拉伯语、缅甸语、中文、丹麦语、荷兰语、英语、芬兰语、法语、德语、希腊语、希伯来语、印地语、印尼语、意大利语、日语、高棉语、韩语、老挝语、马来语、挪威语、波兰语、葡萄牙语、俄语、西班牙语、斯瓦希里语、瑞典语、菲律宾语、泰语、土耳其语、越南语 + +### 5.2 支持的 9 种中国方言 + +四川话、粤语、吴语、东北话、河南话、陕西话、山东话、天津话、闽南话 + +### 5.3 使用方式 + +无需语言标签,直接输入原始文本即可合成。方言通过控制指令描述 + 方言文本词汇实现。 + +--- + +## 6. 音频规格 + +### 6.1 输入/输出 + +| 版本 | 参考音频输入 | 输出采样率 | 输出格式 | +|------|:---:|:---:|:---:| +| VoxCPM2 | 16kHz | **48kHz** | WAV (float32) | +| VoxCPM1.5 | 44.1kHz | 44.1kHz | WAV (float32) | +| VoxCPM-0.5B | 16kHz | 16kHz | WAV (float32) | + +### 6.2 AudioVAE V2 设计 + +VoxCPM2 采用**非对称编解码**设计: +- 编码器:16kHz 输入 → 64 维 latent +- 解码器:64 维 latent → 48kHz 输出 +- 内置超分能力,低质量输入也能输出高质量音频 + +--- + +## 7. 性能指标 + +### 7.1 各平台 RTF + +| 平台 | Python (标准) | Nano-vLLM | llama.cpp-omni | +|------|:---:|:---:|:---:| +| RTX 4090 | ~0.30 | ~0.13 | - | +| Apple M4 (Metal) | ~60 (float32) | - | ~3.6 (timesteps=7) | + +### 7.2 显存/内存占用 + +| 模型 | 显存 (GPU) | 内存 (Mac) | +|------|:---:|:---:| +| VoxCPM2 (Python) | ~8 GB | ~8 GB (float32) | +| VoxCPM2 (GGUF Q8_0) | - | ~3.3 GB | + +--- + +## 8. 生态系统 + +| 项目 | 说明 | 适用场景 | +|------|------|---------| +| Nano-vLLM-VoxCPM | 高吞吐 GPU 推理引擎 | 生产并发服务 | +| vLLM-Omni | 官方 vLLM 全模态服务,OpenAI 兼容 API | 生产多租户部署 | +| llama.cpp-omni | C++ 推理引擎 (CPU/Metal/CUDA/Vulkan) | 端侧/消费级硬件 | +| ComfyUI-VoxCPM | ComfyUI 节点 | 创意工作流 | +| VoxCPM-ONNX | ONNX 导出 | 跨平台 CPU | + +--- + +## 9. 环境要求 + +### 9.1 Python 方式 + +- Python >= 3.10 (< 3.13) +- PyTorch >= 2.5.0 +- CUDA >= 12.0(GPU)/ MPS(Apple Silicon) + +### 9.2 llama.cpp-omni 方式 + +- C++ 编译器 (CMake) +- macOS: 自动启用 Metal +- Linux: 自动检测 CUDA + +### 9.3 安装 + +```bash +pip install voxcpm # 基础安装 +pip install "voxcpm[timestamps]" # 含时间戳功能 +pip install "voxcpm[dev]" # 含开发工具 +``` + +--- + +## 10. 许可证 + +Apache-2.0,免费商用。 diff --git a/test_tts.py b/test_tts.py new file mode 100644 index 00000000..2da50809 --- /dev/null +++ b/test_tts.py @@ -0,0 +1,61 @@ +"""VoxCPM2 TTS quick test - runs on Apple Silicon MPS.""" +import time +import sys + +import soundfile as sf +from voxcpm import VoxCPM + +MODEL_PATH = "./pretrained_models/VoxCPM2" +OUTPUT_EN = "test_output_en.wav" +OUTPUT_ZH = "test_output_zh.wav" + +print("=" * 60) +print("VoxCPM2 TTS Quick Test") +print("=" * 60) + +# Load model +t0 = time.time() +print("\n[1/3] Loading model from local path...") +model = VoxCPM.from_pretrained(MODEL_PATH, load_denoiser=False) +print(f" Loaded in {time.time() - t0:.1f}s") +print(f" Sample rate: {model.tts_model.sample_rate} Hz") + +# Generate English +text_en = "VoxCPM2 is a tokenizer-free text to speech system for multilingual speech generation." +print(f"\n[2/3] Generating English audio...") +print(f" Text: {text_en}") +t0 = time.time() +wav = model.generate( + text=text_en, + cfg_value=2.0, + inference_timesteps=10, + seed=42, +) +gen_time = time.time() - t0 +sf.write(OUTPUT_EN, wav, model.tts_model.sample_rate) +print(f" Generated {len(wav)} samples in {gen_time:.1f}s") +print(f" Audio duration: {len(wav) / model.tts_model.sample_rate:.1f}s") +print(f" RTF (real-time factor): {gen_time / (len(wav) / model.tts_model.sample_rate):.2f}") +print(f" Saved: {OUTPUT_EN}") + +# Generate Chinese +text_zh = "VoxCPM2 是一个无分词器的端到端语音合成系统,支持三十种语言。" +print(f"\n[3/3] Generating Chinese audio...") +print(f" Text: {text_zh}") +t0 = time.time() +wav = model.generate( + text=text_zh, + cfg_value=2.0, + inference_timesteps=10, + seed=42, +) +gen_time = time.time() - t0 +sf.write(OUTPUT_ZH, wav, model.tts_model.sample_rate) +print(f" Generated {len(wav)} samples in {gen_time:.1f}s") +print(f" Audio duration: {len(wav) / model.tts_model.sample_rate:.1f}s") +print(f" RTF (real-time factor): {gen_time / (len(wav) / model.tts_model.sample_rate):.2f}") +print(f" Saved: {OUTPUT_ZH}") + +print("\n" + "=" * 60) +print("Done! All outputs saved.") +print("=" * 60) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 8577c08b..fa1030c0 100644 --- a/uv.lock +++ b/uv.lock @@ -670,7 +670,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -742,7 +742,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1169,7 +1169,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2772,6 +2772,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, ] +[[package]] +name = "openai-whisper" +version = "20250625" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "numba" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tiktoken" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'linux2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/8e/d36f8880bcf18ec026a55807d02fe4c7357da9f25aebd92f85178000c0dc/openai_whisper-20250625.tar.gz", hash = "sha256:37a91a3921809d9f44748ffc73c0a55c9f366c85a3ef5c2ae0cc09540432eb96", size = 803191, upload-time = "2025-06-26T01:06:13.34Z" } + [[package]] name = "orjson" version = "3.11.7" @@ -3952,10 +3968,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -4001,10 +4017,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -4054,7 +4070,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -4115,7 +4131,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4421,13 +4437,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/cb/89c6ef98aa1be0a51297d495eb2bc85b863d841809214ab3fbed8b273a5c/spaces-0.47.0-py3-none-any.whl", hash = "sha256:427d626ac131591d6d52d894d7cb21dfc0125a277279c362c26e6ca154f27d39", size = 105206, upload-time = "2026-01-21T12:02:10.184Z" }, ] +[[package]] +name = "stable-ts" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "openai-whisper" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/d9/d326f9dbbb7da6806aa8cfc080342e5f78dc33552f4339bdc8a6251d11a3/stable_ts-2.19.1.tar.gz", hash = "sha256:0ecaf1ed93e029839569618d2da9a57b883ad04db21f0680146e0650caaf4f52", size = 189132, upload-time = "2025-08-16T16:53:48.811Z" } + [[package]] name = "standard-aifc" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, - { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, + { name = "audioop-lts" }, + { name = "standard-chunk" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } wheels = [ @@ -4448,7 +4478,7 @@ name = "standard-sunau" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "audioop-lts" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } wheels = [ @@ -4517,6 +4547,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + [[package]] name = "tokenizers" version = "0.22.2" @@ -4957,6 +5048,9 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, ] +timestamps = [ + { name = "stable-ts" }, +] [package.metadata] requires-dist = [ @@ -4982,6 +5076,7 @@ requires-dist = [ { name = "sortedcontainers" }, { name = "soundfile" }, { name = "spaces" }, + { name = "stable-ts", marker = "extra == 'timestamps'", specifier = ">=2.19.1" }, { name = "torch", specifier = ">=2.5.0" }, { name = "torchaudio", specifier = ">=2.5.0" }, { name = "torchcodec" }, @@ -4989,7 +5084,7 @@ requires-dist = [ { name = "transformers", specifier = ">=4.36.2" }, { name = "wetext" }, ] -provides-extras = ["dev"] +provides-extras = ["dev", "timestamps"] [[package]] name = "wetext"