进阶 docs.voicebox.sh 2026-10-11 09:58:06 · 25 阅读

第26章 TTS 引擎集成指南

开发者文档:TTS 引擎 如何在 Voicebox 中添加新的文本转语音(TTS)引擎

致人类读者:本文档旨在让 AI 代理能够自主实现新 TTS 引擎的集成。内容按分阶段工作流组织,包含明确关卡和检查清单,代理可据此完成从依赖调研、后端、前端到打包的全流程集成,并交付供本地测试的开发或生产构建草稿。对于自行实现集成的开发者,这也份实用参考。

添加新引擎涉及 4 个层面的约 10 个文件。后端协议实现相对直接,真正的耗时陷阱在于依赖冲突、上游库的 Bug 以及 PyInstaller 打包问题。

在完成第 0 阶段前,严禁开始编写代码。v0.2.3 版本专门发布了三个补丁修复 PyInstaller 问题,根本原因就 是跳过了依赖调研。所有出现的问题——inspect.getsource() 失败、缺失本地数据文件、元数据查找错误、数据类型不匹配——如果在集成开始前阅读模型库源码,其实都是可预判的。

架构概览 后端按层级拆分:

层级 | 用途 | 涉及文件 routes/ | 轻量 HTTP 处理 | 无(自动分发) services/ | 业务逻辑 | 无(自动分发) backends/ | 引擎实现 | your_engine_backend.py utils/ | 共享工具 | 按需

新引擎在后端只需修改 backends/ 和 models.py。路由和服务层使用模型配置注册表,自动处理分发逻辑。

第 0 阶段:依赖调研 此阶段为强制项。在编写任何集成代码前,将模型库及其关键依赖克隆到临时目录并进行检查。目标是生成一份依赖审计报告,识别所有与 PyInstaller 不兼容的模式、所有本地数据文件以及需要规避的上游 Bug。

0.1 克隆并检查模型库 # 创建临时工作区 mkdir /tmp/engine-research && cd /tmp/engine-research

# 克隆模型库 git clone https://github.com/org/model-library.git cd model-library

按以下顺序阅读文件:

setup.py / setup.cfg / pyproject.toml — 检查固定的依赖版本。如果库固定了 torch==2.6.0 或 numpy<1.26,则需要 --no-deps 安装并手动列示子依赖(chatterbox-tts 出现过此情况)。

__init__.py 和主模型类 — 追踪导入链。查找: from_pretrained() — 内部是否调用 huggingface_hub?是否传入 token=True(若无存储的 HF token 会崩溃)? from_local() — 是否存在?可能需要手动 snapshot_download() + from_local() 以绕过下载 Bug。 设备处理 — 默认是否使用 CUDA?是否支持 MPS?许多库在 MPS 上因不支持的操作符而崩溃。

所有 import 语句 — 递归追踪库导入的内容。查找: 链中任意位置的 inspect.getsource()(搜索所有 .py 文件) typeguard / @typechecked 装饰器(在导入时调用 inspect.getsource()) importlib.metadata.version() 或 pkg_resources.get_distribution()(需要 --copy-metadata) lazy_loader(需 --collect-all 以打包 .pyi 桩文件)

0.2 扫描与 PyInstaller 不兼容的模式 对克隆的库及其传递依赖运行以下搜索: # inspect.getsource — 在冻结二进制中若缺少 --collect-all 将崩溃 grep -r "inspect.getsource\|getsource(" .

# typeguard / @typechecked — 在导入时调用 inspect.getsource grep -r "@typechecked\|from typeguard" .

# importlib.metadata — 需要 --copy-metadata grep -r "importlib.metadata\|pkg_resources.get_distribution\|pkg_resources.require" .

# 运行时加载的数据文件 — 需要 --collect-all 或 --collect-data grep -r "Path(__file__).parent\|os.path.dirname(__file__)\|resources_path\|pkg_resources.resource_filename" .

# 本地库路径 — 在冻结构建中可能需要环境变量覆盖 grep -r "/usr/share\|/usr/lib\|/usr/local\|espeak\|phonemize" .

# 缺少 map_location 的 torch.load — 在纯 CPU 构建中会崩溃 grep -r "torch.load(" . | grep -v "map_location"

# HuggingFace token Bug grep -r 'token=True\|token=os.getenv' .

# Float64/Float32 假设 — librosa 返回 float64,许多模型假设 float32 grep -r "torch.from_numpy\|\.double()\|float64" .

# @torch.jit.script — 调用 inspect.getsource(),在冻结构建中崩溃 grep -r "@torch.jit.script\|torch.jit.script" .

# torchaudio.load — torchaudio 2.10+ 需要 torchcodec,建议改用 soundfile.read() grep -r "torchaudio.load\|torchaudio.save" .

# 门禁式 HuggingFace 仓库 — 模型将门禁仓库硬编码为分词器/配置源 grep -r "from_pretrained\|tokenizer_name\|AutoTokenizer" . | grep -i "llama\|meta-llama\|gated"

0.3 在临时虚拟环境中安装并追踪 # 创建隔离的 venv python -m venv /tmp/engine-venv source /tmp/engine-venv/bin/activate

# 安装包(先尝试正常安装) pip install model-package

# 检查是否与当前技术栈冲突 pip install model-package torch==2.10 transformers==4.57.3 numpy>=1.26 # 若失败,需要 --no-deps: pip install --no-deps model-package

# 获取完整依赖树 pip show model-package # 检查 Requires 字段 pip show -f model-package # 列示所有安装文件(查找数据文件)

# 检查非 PyPI 依赖 pip install model-package 2>&1 | grep -i "no matching distribution"

0.4 测试 CPU 模型加载 在编写集成代码前,通过纯 Python 脚本验证模型在 CPU 上可用: import torch # 强制使用 CPU,尽早发现 map_location Bug model = ModelClass.from_pretrained("org/model", device="cpu")

# 使用 float32 音频数组测试(而非 float64) import numpy as np audio = np.random.randn(16000).astype(np.float32) output = model.generate("Hello world", audio) print(f"Output shape: {output.shape}, dtype: {output.dtype}, sample rate: {model.sample_rate}")

若崩溃,说明发现了需通过猴子补丁(monkey-patch)修复的 Bug。常见问题包括:

RuntimeError: expected scalar type Float but found Double → 需进行 float32 转换 RuntimeError: map_location → 需打补丁修复 torch.load RuntimeError: Unsupported operator aten::... → 需跳过 MPS

0.5 生成依赖审计报告 进入第 1 阶段前,记录下以下内容:

PyPI 与非 PyPI 依赖 — 哪些包需要 --find-links、git+https:// 或 --no-deps? 所需的 PyInstaller 指令 — 哪些包需要 --collect-all、--copy-metadata、--hidden-import? 运行时数据文件 — 哪些包附带需打包的数据文件(YAML、预训练权重、音素表、着色器库)? 本地库路径 — 哪些包在系统路径查找数据,而在冻结二进制中不存在? 所需的猴子补丁 — torch.load 的 map_location、float64→float32 转换、MPS 跳过、HF token 绕过等。 采样率 — 引擎输出多少?(24kHz、44.1kHz、48kHz) 模型下载方式 — 由库管理的 from_pretrained() 下载,还是手动 snapshot_download() + from_local()?

此审计报告将成为第 1、4、5 阶段的实施计划。

第 1 阶段:后端实现 1.1 创建后端文件 创建 backend/backends/_backend.py(约 200-300 行),实现 TTSBackend 协议: class YourBackend: """必须满足 TTSBackend 协议。"""

async def load_model(self, model_size: str = "default") -> None: ... async def create_voice_prompt(self, audio_path: str, reference_text: str, use_cache: bool = True) -> tuple[dict, bool]: ... async def combine_voice_prompts(self, audio_paths: list[str], ref_texts: list[str]) -> tuple[np.ndarray, str]: ... async def generate(self, text: str, voice_prompt: dict, language: str = "en", seed: int | None = None, instruct: str | None = None) -> tuple[np.ndarray, int]: ... def unload_model(self) -> None: ... def is_loaded(self) -> bool: ... def _get_model_path(self, model_size: str) -> str: ...

每个引擎的关键决策:

决策 | 选项 | 示例 语音提示存储 | 预计算张量 vs 延迟文件路径 | Qwen 存储张量字典;Chatterbox 存储路径 缓存 | 使用语音提示缓存或跳过 | LuxTTS 使用前缀缓存;Chatterbox 跳过缓存 设备选择 | CUDA / MPS / CPU | Chatterbox 在 macOS 上强制 CPU(因 MPS Bug) 模型下载 | 库处理 vs 手动 snapshot_download | Turbo 使用手动下载以绕过 token=True Bug 采样率 | 引擎特定 | LuxTTS 输出 48kHz,其他均为 24kHz

1.2 语音提示模式 模式 A:预计算张量(Qwen, LuxTTS) encoded = model.encode_prompt(audio_path) return encoded, False # (prompt_dict, was_cached)

模式 B:延迟文件路径(Chatterbox, MLX) return {"ref_audio": audio_path, "ref_text": reference_text}, False

模式 C:混合(新引擎可用) embedding = model.extract_speaker(audio_path) return {"embedding": embedding, "ref_audio": audio_path}, False

若使用缓存,请为缓存键添加前缀: cache_key = "yourengine_" + get_cache_key(audio_path, reference_text)

1.3 注册引擎 在 backend/backends/__init__.py 中:

添加 ModelConfig 条目: ModelConfig( model_name="your-engine", display_name="Your Engine", engine="your_engine", hf_repo_id="org/model-repo", size_mb=3200, needs_trim=False, # 若输出需 trim_tts_output() 则设为 True languages=["en", "fr", "de"], ),

添加到 TTS_ENGINES 字典: TTS_ENGINES = { ... "your_engine": "Your Engine", }

添加工厂分支: elif engine == "your_engine": from .your_backend import YourBackend backend = YourBackend()

1.4 更新请求模型 在 backend/models.py 中:

将引擎名称添加到 GenerationRequest.engine 的正则模式 将新语言代码添加到语言正则中

第 2 阶段:路由与服务集成 借助模型配置注册表,路由和服务层无需针对每个引擎进行分发。所有端点使用 get_model_config()、load_engine_model()、engine_needs_trim()、check_model_loaded() 等注册表辅助函数。

除非引擎在 generate 流水线中需要自定义行为,否则无需修改任何路由或服务文件。

后处理 若模型产生尾部静音,请在 ModelConfig 中设置 needs_trim=True。生成服务会自动应用 trim_tts_output()。

第 3 阶段:前端集成 3.1 TypeScript 类型 在 app/src/lib/api/types.ts 中:

将引擎添加到 GenerationRequest 的引擎联合类型中

3.2 语言映射 在 app/src/lib/constants/languages.ts 中:

在 ENGINE_LANGUAGES 记录中添加条目 若需要,将新语言代码添加到 ALL_LANGUAGES

3.3 引擎/模型选择器 在 app/src/components/Generation/EngineModelSelector.tsx 中:

在 ENGINE_OPTIONS 和 ENGINE_DESCRIPTIONS 中添加条目 若适用,添加到 ENGLISH_ONLY_ENGINES

3.4 表单 Hook 在 app/src/lib/hooks/useGenerationForm.ts 中:

在引擎的 Zod schema 枚举中添加 添加引擎到模型名称的映射 为引擎特定字段更新 payload 构造

注意模型命名不一致。HuggingFace 仓库名称、模型大小标签和 API 模型名称并不总遵循可预测的模式。例如,TADA 的 3B 模型名为 tada-3b-ml(而非 tada-3b),因为它是多语言变体。务必检查实际仓库名称并据此构建前端模型名称映射,而非假设 {engine}-{size}。

3.5 模型管理 在 app/src/components/ServerSettings/ModelManagement.tsx 中:

在 MODEL_DESCRIPTIONS 记录中添加描述 在 voiceModels 过滤条件中添加模型名称

3.6 非克隆引擎(预设语音) 若引擎使用预制语音而非从参考音频进行零样本克隆(例如 Kokoro),需额外集成:

后端: 在 kokoro_backend.py(或你的引擎)中,定义 VOICES 列表,包含 (voice_id, display_name, gender, language) 元组 create_voice_prompt() 应返回 {"voice_type": "preset", "preset_engine": "", "preset_voice_id": ""} generate() 应读取 voice_prompt.get("preset_voice_id") 以选择语音 在模型下载完成后,在 backend/routes/models.py 中添加 seed_preset_profiles("") 调用 backend/services/profiles.py 中的 seed_preset_profiles() 函数创建 voice_type="preset" 的数据库配置

前端: EngineModelSelector 根据 selectedProfile.voice_type 过滤选项: "cloned" 配置 → 仅显示克隆引擎(隐藏 Kokoro) "preset" 配置 → 仅显示该预设的引擎

配置卡片对预设配置显示引擎名称徽章 选择预设配置时,引擎自动切换

预设配置的模式字段: voice_type: "preset"(传统配置为 "cloned") preset_engine: "" — 哪个引擎拥有此语音 preset_voice_id: "" — 引擎特定的语音标识符

对于未来的“设计”语音(文本描述而非音频,例如 Qwen CustomVoice): 使用 voice_type: "designed" 及 design_prompt 字段 create_voice_prompt_for_profile() 已为此类型返回设计提示

第 4 阶段:依赖 利用第 0 阶段的依赖审计报告驱动此阶段。此时你应已清楚所需包、冲突及特殊安装需求。

4.1 Python 依赖 添加到 backend/requirements.txt。根据第 0 阶段结果,有三种安装模式:

正常 PyPI 包: some-model-package>=1.0.0

固定依赖冲突(--no-deps)— 若模型包固定了旧版 torch/numpy/transformers,使用 --no-deps 安装并手动列示子依赖。这是 chatterbox-tts 采用的模式: # 在 justfile / CI 配置中: pip install --no-deps chatterbox-tts

# 在 requirements.txt 中 — 列示每个实际子依赖: conformer>=0.3.2 diffusers>=0.31.0 omegaconf>=2.3.0 resemble-perth>=0.0.2 s3tokenizer>=0.1.6

识别子依赖:pip show chatterbox-tts → 查看 Requires 字段,然后与现有 requirements.txt 交叉参考以避免重复。

非 PyPI 包 — 某些库仅存在于 GitHub 或需要自定义索引: # Git 专用包(无 PyPI 发布) linacodec @ git+https://github.com/ysharma3501/LinaCodec.git Zipvoice @ git+https://github.com/ysharma3501/LuxTTS.git

# 自定义包索引(C 扩展,含平台特定轮子) --find-links https://k2-fsa.github.io/icefall/piper_phonemize.html piper-phonemize>=1.2.0

4.2 解决依赖冲突 添加任何依赖前,检查与现有技术栈的冲突: # 当前技术栈固定版本(近似): # Python 3.12+, torch>=2.10, transformers>=4.57, numpy>=1.26

# 测试兼容性 pip install model-package torch==2.10 transformers==4.57.3 numpy>=1.26

# 若失败,检查包固定的版本: pip show model-package | grep Requires # 查看 setup.py/pyproject.toml 中的版本约束

已知的不兼容模式:

torch==2.6.0 — 许多旧包固定此版本 numpy<1.26 — 与 Python 3.12+ 冲突 transformers==4.46.3 — 许多包固定旧版 transformers onnxruntime 固定版本 — 常与 torch 冲突

4.3 更新安装脚本 依赖需添加到多处:

文件 | 需添加内容 backend/requirements.txt | 包及版本约束 justfile | 若需要,添加 --no-deps 安装行(在 setup-python 和 setup-python-release 目标中) .github/workflows/release.yml | CI 构建步骤中添加相同的 --no-deps 行 Dockerfile | Docker 构建中相同的安装命令

第 5 阶段:PyInstaller 打包 (build_binary.py) 这里是大多数痛苦来源。v0.2.3 版本专门用于修复打包问题——v0.2.1 发布的所有新引擎(LuxTTS, Chatterbox, Chatterbox Turbo)在开发环境正常工作,但在生产构建中失败。不要跳过此阶段。

5.1 在 build_binary.py 中注册引擎 每个新引擎需在 backend/build_binary.py 中有条目。此文件驱动 PyInstaller,是“开发正常,生产崩溃”Bug 的最常见来源。需确定引擎依赖所需的 PyInstaller 指令:

指令 | 功能 | 何时需要 --hidden-import | 包含 PyInstaller 无法通过静态分析检测的模块 | 动态导入、延迟导入、插件架构 --collect-all | 打包源码 .py 文件、数据文件及本地库 | 在导入时调用 inspect.getsource() 的包(如通过 typeguard 的 @typechecked 调用 inflect),或附带预训练模型文件的包(如 perth 附带 .pth.tar + hparams.yaml) --collect-data | 仅打包数据文件(非源码或本地库) | 包含 YAML 配置、词汇文件等的包 --collect-submodules | 打包所有子模块 | 具有深层模块树且 PyInstaller 遗漏的包 --copy-metadata | 复制 importlib.metadata 信息 | 运行时调用 importlib.metadata.version() 或 pkg_resources.get_distribution() 的包。已要求用于:requests, transformers, huggingface-hub, tokenizers, safetensors, tqdm

示例:为新引擎添加 hidden imports 和 collect-all: # 在 build_binary.py 中的 args 列表内: "--hidden-import", "backend.backends.your_engine_backend", "--hidden-import", "your_engine_package", "--hidden-import", "your_engine_package.inference", "--collect-all", "some_dependency_that_uses_inspect_getsource", "--copy-metadata", "some_dependency_that_checks_its_own_version",

5.2 v0.2.3 的经验教训 — 实际故障及其修复 以下是发布新引擎时的实际生产故障。它们在开发环境中通过 python -m uvicorn 测试均正常:

引擎 | 故障 | 根本原因 | 修复 LuxTTS | 导入时 "could not get source code" | inflect 使用 typeguard 的 @typechecked,调用 inspect.getsource() — 需要 .py 源码文件,而非仅字节码 | --collect-all inflect LuxTTS | 未找到 espeak-ng-data | piper_phonemize C 库在 /usr/share/espeak-ng-data/ 查找数据,这在打包中不存在 | --collect-all piper_phonemize + 在运行时设置 ESPEAK_DATA_PATH 环境变量(见 5.3) LuxTTS | Vocos 代码中 inspect.getsource 错误 | clinacodec 和 zipvoice 使用源码内省 | --collect-all linacodec + --collect-all zipvoice Chatterbox | watermark 模型 FileNotFoundError | perth 附带预训练模型文件(hparams.yaml, .pth.tar),PyInstaller 默认不打包 | --collect-all perth 所有引擎 | importlib.metadata 失败 | 冻结二进制不包含 huggingface-hub, transformers 等的包元数据 | 为每个受影响包添加 --copy-metadata 所有引擎 | 下载进度条卡在 0% | 在冻结构建中,huggingface_hub 根据日志级别静默禁用 tqdm 进度条 — 我们的进度追踪器从未收到字节更新 | 在 HFProgressTracker 中强制启用 tqdm 内部计数器 TADA | DAC 中 Snake1d 的 inspect.getsource 错误 | @torch.jit.script 调用 inspect.getsource(),在无 .py 源码文件时失败 | 编写轻量 shim (dac_shim.py) 重新实现 Snake1d,不使用 @torch.jit.script,并在 sys.modules 中注册伪 dac.* 模块 所有引擎 | macOS 上 NameError: name 'obj' is not defined | Python 3.12.0 存在 CPython Bug,当 PyInstaller 重写代码对象时损坏字节码 | 升级到 Python 3.12.13+ 所有引擎 | resource_tracker 子进程崩溃 | 冻结二进制中的 multiprocessing 需在其他操作前调用 freeze_support() | 添加到 server.py 入口点

5.3 运行时冻结构建处理 (server.py) 某些修复无法位于 build_binary.py — 需要运行时检测。入口点 backend/server.py 在任何重量级导入前处理这些情况: # 1. freeze_support() — 必须在任何 multiprocessing 使用前调用 import multiprocessing multiprocessing.freeze_support()

# 2. 本地数据路径 — 重定向 C 库到打包的数据 if getattr(sys, 'frozen', False): _meipass = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable)) _espeak_data = os.path.join(_meipass, 'piper_phonemize', 'espeak-ng-data') if os.path.isdir(_espeak_data): os.environ.setdefault('ESPEAK_DATA_PATH', _espeak_data)

# 3. stdout/stderr 安全 — PyInstaller --noconsole 在 Windows 上将这些设为 None if not _is_writable(sys.stdout): sys.stdout = open(os.devnull, 'w')

若引擎依赖包含在系统路径查找数据的本地库(如 espeak-ng),需在此处添加类似的 os.environ.setdefault() 块。

5.4 CUDA 与 CPU 构建分支 build_binary.py 生成两个不同的二进制文件:

voicebox-server (CPU) — 排除所有 nvidia.* 包,避免打包约 3 GB 的 CUDA DLL voicebox-server-cuda — 包含 torch.cuda 和 torch.backends.cudnn

在 Windows 上,若构建环境安装了 CUDA torch 但需构建 CPU 二进制,脚本会临时切换到 CPU-only torch,并在之后恢复 CUDA torch。这防止 PyInstaller 意外将 CUDA 库打包到 CPU 构建中。

新引擎的导入放在公共部分(而非 CUDA 或 MLX 条件块中),除非引擎有平台特定依赖。

5.5 MLX 条件包含 Apple Silicon 构建有条件地包含 MLX 的 hidden imports 和 --collect-all mlx / --collect-all mlx_audio。若引擎有 MLX 特定后端变体,请在 if is_apple_silicon() and not cuda: 块内添加其导入。

5.6 测试冻结构建 不可跳过。在 python -m uvicorn 中正常工作的模型会在 PyInstaller 二进制中崩溃。v0.2.3 版本需三个补丁发布(v0.2.1 → v0.2.2 → v0.2.3)才能在生产环境中让所有引擎正常工作。

构建:just build 直接启动二进制文件(而非通过 python -m) 测试完整链路:下载 → 加载 → 生成 → 进度追踪 检查 stderr 以获取实际错误(日志流向 stderr 以便 Tauri 副进程捕获) 修复,重建,重复

常见陷阱:仅使用开发安装中预缓存的模型测试生成。务必使用干净的模型缓存测试,以验证下载也正常工作。

第 6 阶段:常见上游规避措施 torch.load 设备不匹配 _original_torch_load = torch.load def _patched_torch_load(*args, kwargs): kwargs.setdefault("map_location", "cpu") return _original_torch_load(*args, kwargs) torch.load = _patched_torch_load

Float64/Float32 数据类型不匹配 original_fn = SomeClass.some_method def patched_fn(self, *args, kwargs): result = original_fn(self, *args, kwargs) return result.float() SomeClass.some_method = patched_fn

HuggingFace token Bug from huggingface_hub import snapshot_download local_path = snapshot_download(repo_id=REPO, token=None) model = ModelClass.from_local(local_path, device=device)

MPS 张量问题 若操作符不受支持,完全跳过 MPS: def _get_device(self): if torch.cuda.is_available(): return "cuda" return "cpu" # 跳过 MPS

硬编码的门禁 HuggingFace 仓库作为配置源 某些模型将门禁 HuggingFace 仓库硬编码为分词器或配置源(例如 TADA 在 AlignerConfig 和 TadaConfig 中均硬编码 "meta-llama/Llama-3.2-1B")。在无 HF 认证的情况下,这会静默失败。

修复:从未门禁镜像下载并直接打补丁配置对象: # 从未门禁镜像下载分词器 UNGATED_TOKENIZER = "unsloth/Llama-3.2-1B" tokenizer_path = snapshot_download(UNGATED_TOKENIZER, token=None)

# 打补丁模型配置以使用本地路径而非门禁仓库 config = ModelConfig.from_pretrained(model_path) config.tokenizer_name = tokenizer_path model = ModelClass.from_pretrained(model_path, config=config)

切勿对 AutoTokenizer.from_pretrained 打猴子补丁 — 它是类方法,替换它会破坏描述符,导致使用不同分词器的其他引擎(例如通过 AutoTokenizer 使用 Qwen 分词器的 Qwen)崩溃。始终在配置层面打补丁,而非类方法层面。

torchaudio.load() 在 2.10+ 需要 torchcodec 自 torchaudio>=2.10 起,torchaudio.load() 需要 torchcodec 包进行音频 I/O。若引擎或后端代码使用 torchaudio.load(),请替换为 soundfile: # 之前(无 torchcodec 会崩溃): import torchaudio waveform, sr = torchaudio.load("audio.wav")

# 之后: import soundfile as sf import torch data, sr = sf.read("audio.wav", dtype="float32") waveform = torch.from_numpy(data).unsqueeze(0)

注意:torchaudio.functional.resample() 和其他纯 PyTorch 数学函数在无 torchcodec 时正常工作 — 仅 I/O 函数受影响。

@torch.jit.script 在冻结构建中崩溃 torch.jit.script 调用 inspect.getsource() 以解析被装饰函数的源码。在 PyInstaller 二进制中,.py 源码文件不可用,因此在导入时崩溃。

修复:移除或避免 @torch.jit.script 装饰器。若被装饰函数来自上游依赖,编写 shim 重新实现函数而不使用该装饰器(见下文“有毒依赖链”)。

有毒依赖链 — Shim 模式 有时模型库依赖一个具有庞大且敌意传递依赖树的包,但只使用了其中很小一部分。当依赖链无法构建或会引入数十个不需要的包时,正确做法是编写轻量 shim。

示例:TADA 依赖 descript-audio-codec (DAC),后者引入 descript-audiotools -> onnx, tensorboard, protobuf, matplotlib, pystoi 等。onnx 包在 macOS 上无法从源码构建。但 TADA 仅使用 DAC 中的 Snake1d — 一个 7 行的 PyTorch 模块。

解决方案:在 backend/utils/dac_shim.py 创建 shim,在 sys.modules 中注册伪模块: import sys import types import torch from torch import nn

def snake(x, alpha): """Snake 激活 — 不使用 @torch.jit.script 重新实现。""" return x + (1.0 / (alpha + 1e-9)) * torch.sin(alpha * x).pow(2)

class Snake1d(nn.Module): def __init__(self, channels): super().__init__() self.alpha = nn.Parameter(torch.ones(1, channels, 1)) def forward(self, x): return snake(x, self.alpha)

# 注册伪 dac.* 模块,使 "from dac.nn.layers import Snake1d" 正常工作 _nn = types.ModuleType("dac.nn") _layers = types.ModuleType("dac.nn.layers") _layers.Snake1d = Snake1d _nn.layers = _layers

for name, mod in [("dac", types.ModuleType("dac")), ("dac.nn", _nn), ("dac.nn.layers", _layers)]: sys.modules[name] = mod

Shim 的关键规则:

在导入模型库前导入 shim(以便其先找到伪模块) 切勿在 shim 中使用 @torch.jit.script(见上文) 仅重新实现模型实际使用的部分 — 仔细检查导入链

候选引擎 docs/PROJECT_STATUS.md 文件是候选者评估的正式、动态列表 — 包括某些被积压的原因(例如 VoxCPM,实际上上游仅限 CUDA)。

一眼概览,当前主要候选者:

模型 | 层级 | 大小 | 跨平台? | 关键特性 MOSS-TTS-Nano | 1 | 0.1 B | 是(CPU 实时) | 48 kHz 立体声,Apache 2.0,发布于 2026-04-13 Voxtral TTS | 2 | 4 B | 可能 | mistralai/Voxtral-4B-TTS-2603 — 预设 + 克隆 VibeVoice | 2 | ~500 M | 是 | 播客风格多说话者对话 Dia2 | 3 | TBD | TBD | 原始 Dia 的继任者 Fish Audio S2 Pro | 3 | 中 | 是 | 通过内联文本进行单词级控制

积压:

VoxCPM (2B, Apache 2.0) — 上游需要 CUDA ≥12;MPS 在 issue #232/#248 中损坏;CPU 路径被维护者拒绝 (#256)。继续关注放宽设备要求的 PR。

当你接手或标记某个引擎为已发布/积压时,请更新 PROJECT_STATUS.md。

实施检查清单 将此作为阶段间的关卡。在当前阶段的所有项目未勾选前,不要进入下一阶段。

第 0 阶段:依赖调研

已将模型库源码克隆到临时目录 已阅读 setup.py / pyproject.toml — 记录了固定的依赖版本 已追踪从模型类到叶依赖的所有导入 已在完整依赖树中搜索 inspect.getsource, @typechecked, typeguard 已在依赖树中搜索 importlib.metadata, pkg_resources.get_distribution 已搜索 Path(__file__).parent, os.path.dirname(__file__), 硬编码系统路径 已搜索缺少 map_location 的 torch.load 调用 已搜索未进行 .float() 转换的 torch.from_numpy 已搜索 HuggingFace 调用中的 token=True 或 token=os.getenv("HF_TOKEN") 已搜索 @torch.jit.script / torch.jit.script(在冻结构建中崩溃) 已搜索 torchaudio.load / torchaudio.save(torchaudio 2.10+ 需要 torchcodec) 已搜索硬编码的门禁 HuggingFace 仓库名称(例如,meta-llama/*) 已评估是否有任何依赖可被最小化使用从而通过 shim 代替安装 已在临时 venv 中测试 CPU 上的模型加载和生成 已使用干净的 HuggingFace 缓存测试(无预下载模型) 已生成书面依赖审计报告,记录所有发现

第 1 阶段:后端实现

已创建 backend/backends/_backend.py 以实现 TTSBackend 协议 已选择语音提示模式(预计算张量 vs 延迟文件路径) 已实施第 0 阶段识别的所有猴子补丁 已从 backends/base.py 使用 get_torch_device() 进行设备选择 已从 backends/base.py 使用 model_load_progress() 进行下载/加载追踪 已测试:模型正确下载 已测试:模型在 CPU 上加载 已测试:生成产生有效音频 已测试:从参考音频进行语音克隆有效 已在 backends/__init__.py 中注册 ModelConfig 已添加到 TTS_ENGINES 字典 已在 get_tts_backend_for_engine() 中添加工厂分支 已更新 backend/models.py 中的引擎正则

第 2–3 阶段:路由、服务和前端

已确认路由/服务无需更改(或记录了为何需要自定义行为) 已在 app/src/lib/api/types.ts 中将引擎添加到 TypeScript 联合类型 已在 app/src/lib/constants/languages.ts 中添加语言映射条目 已在 EngineModelSelector.tsx 中添加到 ENGINE_OPTIONS 和 ENGINE_DESCRIPTIONS 已在 useGenerationForm.ts 中添加到 Zod schema 和模型名称映射 已在 ModelManagement.tsx 中添加描述

第 4 阶段:依赖

已将包添加到 backend/requirements.txt 若需要 --no-deps:已显式列示子依赖 若有 Git 专用包:已添加 @ git+https://... 条目 若需自定义索引:已添加 --find-links 行 已更新 justfile 设置目标 已更新 .github/workflows/release.yml 构建步骤 若适用,已更新 Dockerfile 已在干净的 venv 中验证 pip install 成功(含现有 requirements)

第 5 阶段:PyInstaller 打包

已在 build_binary.py 中为以下添加 --hidden-import 条目: backend.backends._backend 模型包及其关键子模块

已为以下包添加 --collect-all: 使用 inspect.getsource() / @typechecked 附带预训练模型数据文件(.pth.tar, .yaml 等) 附带本地数据文件(音素表、着色器库等)

已为使用 importlib.metadata 的包添加 --copy-metadata 若引擎有本地数据路径:已在 server.py 中添加 os.environ.setdefault() 已使用 just build 构建冻结二进制 已在冻结二进制中使用干净模型缓存测试(非开发预缓存): 模型下载具有实时进度 模型加载有效 生成产生有效音频 stderr 日志无错误

第 6 阶段:最终验证

引擎在开发模式工作 (just dev) 引擎在冻结二进制工作 (just build → 直接运行二进制) 已在目标平台测试(macOS 用于 MLX,Windows/Linux 用于 CUDA) 现有引擎无回归

在 GitHub 上编辑 | TTS 生成 | 文本转语音生成如何在 Voicebox 的多引擎后端中工作 | 效果流水线 | 音频后处理效果及生成版本控制

评论 (0)