一、问题现象
启动后端服务后,调用 TTS API (/v1/audio/speech) 返回 HTTP 500 错误。
二、问题排查与解决过程
1. No module named 'soundfile'
原因: 后端虚拟环境 .venv 中缺少 soundfile 包。
解决:
cd qwen3-voice-service/backend && source .venv/bin/activate && pip install soundfile
预防: 在 pyproject.toml 的 dependencies 中添加 "soundfile>=0.13.0"。
2. No module named 'mlx_audio'
原因: 虚拟环境中未安装 mlx-audio。之前尝试通过 [mlx] 可选依赖安装,但 uv 解析失败。
解决:
cd qwen3-voice-service/backend && source .venv/bin/activate && pip install "mlx-audio>=0.4.3"
注意: mlx-audio 必须 >=0.4.3,因为该版本才在 MODEL_REMAPPING 中添加了 "qwen3_tts"。
3. No module named 'modelscope'
原因: ASR 模块需要 modelscope,但虚拟环境中未安装。
解决:
cd qwen3-voice-service/backend && source .venv/bin/activate && pip install modelscope
4. ValueError: Received 904 parameters not in model
原因: mlx_audio 在加载模型时,根据 config.json 中的 model_type 路由到不同的模型类。Qwen3-TTS 的 config.json 中 model_type 为 "qwen3_tts",但 mlx_audio 0.4.2 及更早版本没有这个映射,被错误路由到 qwen3 类。
解决:
- 升级
mlx-audio到 >=0.4.3,该版本在MODEL_REMAPPING中添加了"qwen3_tts": "qwen3_tts"。 - 在
_load_mlx_tts()中确保 config.json 的model_type被正确设置为"qwen3_tts"。
5. TypeError: ModelConfig.__init__() missing 12 required keyword arguments
原因: mlx_audio 的 ModelConfig 继承自 mlx_lm.models.qwen3.ModelArgs,期望 config.json 顶层有 hidden_size, num_hidden_layers, num_attention_heads 等字段。但 Qwen3-TTS 的 config.json 将这些参数嵌套在 talker_config 下。
解决: 修改 _load_mlx_tts(),在加载 config.json 后自动将 talker_config 中的字段 flatten 到顶层:
if "talker_config" in config:
for key, val in config["talker_config"].items():
if key not in ("model_type", "architectures"):
config[key] = val
6. ValueError: Model directory not found — LMStudio 模型无法使用
原因: LMStudio 下载的 Qwen3-TTS 模型不完整,缺少 speech_tokenizer/ 子目录。mlx_audio 加载时会检查该目录是否存在。
解决:
- 从 HuggingFace 下载完整模型(包含
speech_tokenizer/):
from huggingface_hub import snapshot_download
for repo_id in [
"iic/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"iic/Qwen3-TTS-12Hz-0.6B-CustomVoice",
"iic/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
"iic/Qwen3-TTS-12Hz-1.7B-Base",
"iic/Qwen3-TTS-12Hz-0.6B-Base",
]:
snapshot_download(repo_id, local_dir=f"/tmp/hf-tts-complete/{repo_id.split('/')[-1]}")
- 将下载好的模型复制到 LMStudio 目录:
cp -r /tmp/hf-tts-complete/* ~/.lmstudio/models/mlx-community/
- 在
model_path.py中添加 LMStudio 目录检测,使resolve_model_path()能自动找到本地模型。
7. uv run 解析失败 — [mlx] 和 [cuda] 可选依赖冲突
原因: pyproject.toml 中同时定义了 [mlx](依赖 mlx-audio>=0.4.3,间接要求 transformers>=5.5.0)和 [cuda](依赖 qwen-tts>=0.1.1,固定要求 transformers==4.57.3)。uv 解析时发现两个依赖对 transformers 的要求冲突,导致整个项目无法解析。
解决: 从 pyproject.toml 中移除 [mlx] 和 [cuda] 可选依赖。相关包(mlx-audio, qwen-tts, torch 等)已在 .venv 中手动安装,不需要通过可选依赖引入。
三、最终方案总结
架构决策
- 使用 HuggingFace 而非 LMStudio 下载模型 — LMStudio 的 Qwen3-TTS 模型不完整,缺少
speech_tokenizer/。 - 手动管理虚拟环境依赖 — 由于 uv 的可选依赖解析冲突,不在
pyproject.toml中声明 mlx/cuda 相关包。 - 代码自动修复模型配置 —
_load_mlx_tts()中实现 config.json flatten 和 model_type 修正,避免手动修改模型文件。
依赖安装清单(在 .venv 中)
| 包 | 版本要求 | 用途 |
|---|---|---|
mlx-audio | >=0.4.3 | TTS 推理引擎 |
mlx-lm | >=0.31.0 | MLX 模型加载 |
qwen-tts | >=0.1.1 | TTS 模型(CUDA 模式) |
torch | >=2.5.0 | CUDA 推理后端 |
modelscope | >=1.0.0 | ASR 模型下载 |
soundfile | >=0.13.0 | WAV 文件读写 |
修改的文件
| 文件 | 变更内容 |
|---|---|
backend/src/engines/local_engine.py | _load_mlx_tts() 自动 flatten config + 修正 model_type |
backend/src/utils/model_path.py | resolve_model_path() 支持 LMStudio 目录检测 |
backend/pyproject.toml | 移除 [mlx] 和 [cuda] 可选依赖,添加 soundfile |
模型目录结构(正确)
~/.lmstudio/models/mlx-community/Qwen3-TTS-12Hz-{1.7B|0.6B}-{CustomVoice|Base|VoiceDesign}-8bit/
├── config.json # model_type: "qwen3_tts", 顶层有 hidden_size 等字段
├── model.safetensors
├── model_config.json # Qwen3-TTS 原始配置
├── preprocessor_config.json
├── tokenizer.json
├── tokenizer.model
├── tokenizer_config.json
└── speech_tokenizer/ # LMStudio 下载时缺失的关键目录
├── config.json
├── model.safetensors
├── tokenizer.json
└── tokenizer_config.json
---
## 四、后续问题修复(0529 之后)
### 8. voice_design 模式返回 500 — instruct 参数未传递
**问题:** 用户调用 voice_design 模式时,curl 请求中传了 `"mode": "voice_design"` 但没传 `instruct`,后端返回:
ValueError: instruct (voice description) is required for voice_design mode
HTTP 500 Internal Server Error
**根因分析(三层问题):**
#### 8.1 Engine 层 — mode 自动推断缺失
**原因:** `local_engine.py` 中,当用户 curl 请求不传 `mode` 字段时,默认值为 `"custom_voice"`。如果用户只传了 `instruct`(声音描述)但没传 `mode`,系统无法自动推断为 voice_design 模式。
**解决:** 在 `MLXTTSEngine` 和 `PyTorchTTSEngine` 的 `synthesize()` 中增加 mode 自动推断逻辑:
python
mode = kwargs.get(“mode”) or “custom_voice”
instruct = kwargs.get(“instruct”, emotion)
Auto-infer mode from parameters if not explicitly set
if mode == “custom_voice” and instruct and not voice:
mode = “voice_design”
**测试:** `tests/test_tts_mode_inference.py` — 4 个测试覆盖推断场景。
#### 8.2 API 层 — ValueError 返回 500 而非 422
**原因:** `tts.py` 中所有异常统一返回 HTTP 500。`ValueError`(参数验证错误)属于客户端请求问题,应返回 422。
**解决:** 在 `tts.py` 中增加 `ValueError` 捕获:
python
except ValueError as e:
logger.warning(f”TTS validation error: {e}”)
raise HTTPException(status_code=422, detail=f”Invalid request: {str(e)}”)
**测试:** `tests/test_tts_voice_design_errors.py` — 4 个测试覆盖错误场景。
#### 8.3 Frontend — voice_design 模式下 instruct 字段未发送
**原因:** `TTSPanel.tsx` 中 voice_design 模式下,用户输入的声音描述存在 `emotion` React state 变量中,但构建请求体时**没有将值发送到任何字段**。前端复用了 `emotion` 状态名用于两种不同场景:
- custom_voice → 情绪(happy/calm)→ `req.emotion`
- voice_design → 声音描述 → **应该发到 `req.instruct`,但漏发了**
后端虽有 fallback `instruct = kwargs.get("instruct", emotion)`,但前端根本没发 `emotion` 字段(因为 voice_design 分支里没有设置任何请求字段)。
**解决:** `TTSPanel.tsx:39` — voice_design 分支增加 `req.instruct = emotion`:
typescript
} else if (mode === ‘voice_design’) {
if (!emotion) { showToast(‘请输入声音描述’, ‘error’); return }
req.instruct = emotion
}
**测试:** `TTSPanel.instruct.test.tsx` — 3 个测试验证 instruct/emotion 字段在不同模式下的正确发送。
**前端测试:** `npx vitest run` — 115/115 全部通过。
---
### 9. 模型自动下载导致启动时触发大量下载
**问题:** 首次调用 TTS/ASR API 时,如果模型不在本地,`resolve_model_path()` 会自动从 ModelScope/HuggingFace 下载整个模型仓库(数百 MB),用户无法控制且无提示。
**根因:** `model_path.py` 中 `resolve_model_path()` 的第三分支无条件调用 `snapshot_download()`,没有任何检查或提示。
**解决:**
1. **移除自动下载逻辑** — `resolve_model_path()` 在模型不在本地时抛出 `ModelNotAvailableError`,包含:
- 模型 ID
- ModelScope 下载链接(预定义所有已知模型的 URL)
- 放置路径提示 (`~/.lmstudio/models/`)
- `.env` 配置路径说明
2. **API 层返回友好错误** — `tts.py` 和 `asr.py` 捕获 `ModelNotAvailableError`,返回 HTTP 404 + 完整下载提示。
3. **更新 `.env.example`** — 注释说明自动下载已禁用,引导用户手动下载。
4. **新增 `ModelNotAvailableError` 异常类** — 专门用于模型缺失场景,与业务逻辑的 `ValueError` 区分。
**测试:** `tests/test_model_path_no_auto_download.py` — 3 个测试验证行为。
**后端测试:** `uv run pytest -q` — 84/84 全部通过。
---
## 五、最终方案总结(更新)
### 架构决策
1. **使用 HuggingFace 而非 LMStudio 下载模型** — LMStudio 的 Qwen3-TTS 模型不完整,缺少 `speech_tokenizer/`。
2. **手动管理虚拟环境依赖** — 由于 uv 的可选依赖解析冲突,不在 `pyproject.toml` 中声明 mlx/cuda 相关包。
3. **代码自动修复模型配置** — `_load_mlx_tts()` 中实现 config.json flatten 和 model_type 修正,避免手动修改模型文件。
4. **禁用自动下载** — 模型不存在时抛出明确错误并给出下载链接,由用户手动下载。
5. **mode 自动推断** — 当未显式指定 mode 但有 instruct 参数时,自动推断为 voice_design。
6. **前端字段映射正确性** — voice_design 模式下声音描述发到 `instruct` 字段,custom_voice 模式下情绪发到 `emotion` 字段。
### 依赖安装清单(在 `.venv` 中)
| 包 | 版本要求 | 用途 |
|---|---|---|
| `mlx-audio` | >=0.4.3 | TTS 推理引擎 |
| `mlx-lm` | >=0.31.0 | MLX 模型加载 |
| `qwen-tts` | >=0.1.1 | TTS 模型(CUDA 模式) |
| `torch` | >=2.5.0 | CUDA 推理后端 |
| `modelscope` | >=1.0.0 | ASR 模型下载 |
| `soundfile` | >=0.13.0 | WAV 文件读写 |
### 修改的文件(全部)
| 文件 | 变更内容 |
|---|---|
| `backend/src/engines/local_engine.py` | `_load_mlx_tts()` 自动 flatten config + 修正 model_type;engine synthesize 中增加 mode 自动推断 |
| `backend/src/utils/model_path.py` | `resolve_model_path()` 支持 LMStudio 目录检测;移除自动下载,新增 `ModelNotAvailableError` |
| `backend/src/api/tts.py` | 增加 logging;捕获 `ValueError` → 422,`ModelNotAvailableError` → 404 |
| `backend/src/api/asr.py` | 增加 logging;捕获 `ModelNotAvailableError` → 404 |
| `backend/pyproject.toml` | 移除 `[mlx]` 和 `[cuda]` 可选依赖,添加 `soundfile` |
| `frontend/src/pages/TTSPanel.tsx` | voice_design 模式下将声音描述发到 `req.instruct` 字段 |
| `backend/.env.example` | 更新注释说明自动下载已禁用 |
### 新增测试文件
| 文件 | 覆盖内容 |
|---|---|
| `backend/tests/test_tts_instruct.py` | instruct 参数处理(4 tests) |
| `backend/tests/test_tts_voice_design.py` | voice_design 模式生成器行为(2 tests) |
| `backend/tests/test_tts_language.py` | 语言代码转换(4 tests) |
| `backend/tests/test_tts_mode_inference.py` | mode 自动推断(4 tests) |
| `backend/tests/test_tts_voice_design_errors.py` | voice_design 错误处理(4 tests) |
| `backend/tests/test_model_path_no_auto_download.py` | 模型缺失时不自动下载(3 tests) |
| `frontend/src/pages/TTSPanel.instruct.test.tsx` | instruct/emotion 字段映射(3 tests) |
### 测试统计
| 环境 | 测试数 | 状态 |
|---|---|---|
| Backend (pytest) | 84 passed | ✅ |
| Frontend (vitest) | 115 passed | ✅ |
### 模型目录结构(正确)
~/.lmstudio/models/mlx-community/Qwen3-TTS-12Hz-{1.7B|0.6B}-{CustomVoice|Base|VoiceDesign}-8bit/
├── config.json # model_type: “qwen3_tts”, 顶层有 hidden_size 等字段
├── model.safetensors
├── model_config.json # Qwen3-TTS 原始配置
├── preprocessor_config.json
├── tokenizer.json
├── tokenizer.model
├── tokenizer_config.json
└── speech_tokenizer/ # LMStudio 下载时缺失的关键目录
├── config.json
├── model.safetensors
├── tokenizer.json
└── tokenizer_config.json
“`