Qwen3-tts模型 API 服务化过程问题总结

一、问题现象

启动后端服务后,调用 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 加载时会检查该目录是否存在。

解决:

  1. 从 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]}")
  1. 将下载好的模型复制到 LMStudio 目录:
   cp -r /tmp/hf-tts-complete/* ~/.lmstudio/models/mlx-community/
  1. 在 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 中手动安装,不需要通过可选依赖引入。


三、最终方案总结

架构决策

  1. 使用 HuggingFace 而非 LMStudio 下载模型 — LMStudio 的 Qwen3-TTS 模型不完整,缺少 speech_tokenizer/。
  2. 手动管理虚拟环境依赖 — 由于 uv 的可选依赖解析冲突,不在 pyproject.toml 中声明 mlx/cuda 相关包。
  3. 代码自动修复模型配置 — _load_mlx_tts() 中实现 config.json flatten 和 model_type 修正,避免手动修改模型文件。

依赖安装清单(在 .venv 中)

包版本要求用途
mlx-audio>=0.4.3TTS 推理引擎
mlx-lm>=0.31.0MLX 模型加载
qwen-tts>=0.1.1TTS 模型(CUDA 模式)
torch>=2.5.0CUDA 推理后端
modelscope>=1.0.0ASR 模型下载
soundfile>=0.13.0WAV 文件读写

修改的文件

文件变更内容
backend/src/engines/local_engine.py_load_mlx_tts() 自动 flatten config + 修正 model_type
backend/src/utils/model_path.pyresolve_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
“`

发表回复