本地 TTS 服务(tts_server_runtime)
想接 Qwen3-TTS(含 3 秒克隆音色 / CustomVoice / VoiceDesign)又不想自己写推理? 宿主自带一份
tts-server.exe(qwentts.cpp,MIT), 走 OpenAI 兼容 HTTP。插件只负责起进程 + 发请求 + 管音色。运行库随安装包内置(
bundled),用户不需要点「下载运行库」,也不需要联网。
1) 声明与取用
{
"requires_runtime": [
{ "id": "tts_server_runtime", "reason": "提供 tts-server 运行时(qwentts.cpp,随安装包内置)" }
]
}from pathlib import Path
rt = self.host.runtime_dir("tts_server_runtime") # 就绪返回目录,否则 ""
if not rt:
return False, "model_not_ready", "缺少「TTS-Server 运行库」;0.2.0+ 默认内置,缺失请重装"
exe = Path(rt) / "tts-server.exe"不要自己下载 / 解包 / 随包携带这份二进制,也不要写进 model_artifacts。 目录里除了 tts-server.exe 还有 MinGW 运行库(libgomp-1.0.dll、libstdc++-6.dll、 libgcc_s_seh-1.dll、libwinpthread-1.dll)与 vulkan-1.dll——必须整目录一起用, 只拷 exe 会在别的机器上起不来。
旧写法(仅 0.1.x):当时由承载插件
workshop.common_tts_server提供,host.plugin_dir("workshop.common_tts_server") / "tts-server" / "tts-server.exe"在当时可用。 0.2.0 已没有这个承载插件,老写法返回空路径;新插件一律用runtime_dir()。
2) 启动参数
args = [
str(exe),
"--model", str(talker_gguf), # 必填:talker LM(qwen-talker-*.gguf)
"--codec", str(tokenizer_gguf), # 必填:codec(qwen-tokenizer-*.gguf)
"--device", "Vulkan0", # CUDA0 / Vulkan0 / ROCm0 / MTL0 / CPU
"--host", "127.0.0.1",
"--port", str(port),
"--alias", "Qwen3-TTS-1.7B", # 出现在 /v1/models 里,用于确认「这个端口上的是我的服务」
]| 参数 | 说明 |
|---|---|
--model / --codec | 两个 GGUF 必须同时给;--alias 建议填一个稳定名字 |
--device | CUDA0 / Vulkan0 / ROCm0 / MTL0 / CPU(不是 cuda / vulkan,见 §5) |
--port | 建议 18xxx 高位端口:8080/8081 常年被 Steam 的 steamwebhelper 占用 |
--no-fa | 关 flash attention(排障用) |
--codec-chunk-dur | wav 响应的 codec 解码块时长(秒,默认 24) |
启动后轮询 GET /health 直到 200。首次加载要 10s 量级(1.7B Q8),别用 3 秒超时判死。
3) HTTP 契约(实测 v1.0.0)
3.1 GET /health → {"status":"ok"}
就绪判定用它;不要用「端口能连上」代替(服务在加载权重期间就已监听)。
3.2 GET /v1/models → 用于认领端口
{"object":"list","data":[{"id":"Qwen3-TTS-1.7B","object":"model","owned_by":"local"}]}id 就是你启动时给的 --alias。宿主机上可能同时跑着别的插件起的服务, 必须通过它确认「这个端口上的是我启动的那一个」,否则会把别人的服务当自己的复用。
3.3 POST /v1/audio/speech
{
"model": "Qwen3-TTS-1.7B",
"input": "要合成的文本",
"voice": "音色名或留空",
"response_format": "wav",
"max_new_tokens": 400,
"instructions": "(仅 voicedesign)年轻女性,温柔"
}| 字段 | 说明 |
|---|---|
input | 待合成文本 |
voice | base 变体:已注册的克隆音色名(传文件路径会被判 400 unknown voice);customvoice 变体:内置 speaker 名;留空 = 服务端默认音色 |
response_format | wav(整段一次性)/ pcm(24 kHz 单声道 s16le 裸流,可配合 stream=true 边生成边吐) |
max_new_tokens | 强烈建议设上限:12 Hz 码率下 token ≈ 2.5~3.5 × 字符数。不设上限时,参考文本与音频不匹配的 ICL 克隆会退化成长时间不自停(实测 7 字输入生成 164 秒音频),而合成 worker 是单线程 → 整段朗读卡死 |
instructions | 仅 voicedesign 变体:用文字描述音色 |
返回:200 + 音频字节(wav 为 RIFF,pcm 为裸 s16le)。错误为 4xx/5xx + JSON。
3.4 克隆音色注册表(只在内存里)
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /v1/audio/voices | → {"voices":[{"name":"x","kind":"registered"}]} |
POST | /v1/audio/voices | body {"name":"x","ref_text":"参考音频的文本","wav_b64":"<base64 WAV>"} |
DELETE | /v1/audio/voices/<name> | 删除 |
注意:GET /v1/voices 不存在(404),别照 OpenAI 老文档写。
⚠️
tts-server的音色注册表不落盘:进程重启即清空。 插件必须自己做持久化 + 就绪后自动补注册(见 本地服务型引擎强制契约 §7)。
4) 模型变体(三选一)
同一仓库 Serveurperso/Qwen3-TTS-GGUF 里:
| 变体 | talker 文件 | 能力 |
|---|---|---|
base | qwen-talker-1.7b-base-Q4_K_M.gguf | 参考音频克隆音色(无内置 speaker) |
customvoice | qwen-talker-1.7b-customvoice-Q4_K_M.gguf | 内置预设音色,voice 直接选 |
voicedesign | qwen-talker-1.7b-voicedesign-Q4_K_M.gguf | 用文字描述生成音色 |
三者共用 qwen-tokenizer-12hz-Q8_0.gguf。把每个变体声明成独立的 model_artifacts, 并在 model_artifacts() 里把当前生效的那份排第一位(见 模型工件)。
5) 计算设备:--device 取设备名,不是 cuda / vulkan
def _device_arg(device: str) -> str:
dev = (device or "vulkan").strip().lower()
if dev == "cpu":
return "CPU"
if dev in ("cuda", "cuda0", "cuda13"):
return "CUDA0"
if dev in ("rocm", "hip", "rocm0"):
return "ROCm0"
if dev in ("metal", "mtl", "mtl0"):
return "MTL0"
return "Vulkan0"⚠️ 随包内置的是 Vulkan 构建,没有独立 CUDA 版
tts-server.exe 的 --help 里写着 CUDA0,但那只是通用帮助文案。 用二进制符号实测:内嵌 ggml_vk 符号 461 个、ggml_cuda/cuda_backend 0 个; 传 --device CUDA0 会直接回:
--device CUDA0 not found. Available: Vulkan0 Vulkan1 CPU所以插件配置里只暴露 vulkan / cpu 两项,别把 cuda 写进 choices—— 用户选了会直接起不来。
为什么不强上 CUDA:Vulkan 跑本模型 RTF 0.18~0.26(见 §6),离实时线 1.0 还有 4~5 倍余量, 换 CUDA 的收益在实时朗读场景里观测不到。真要 CUDA 只能自己用 buildcuda 重编 qwentts.cpp。
试图用 nv-pack 里的
ggml-cuda.dll动态挂载(GGML_BACKEND_PATH)实测不可行: 能加载、能 init 出 CUDA 设备,但载完权重就在[Pipeline] Ready之后崩溃 (ggml ABI 版本不匹配:tts-server 是 0.26.x,nv-pack 是 0.27.x)。
6) 性能参考(RTX 4070 Ti,1.7B Q4_K_M talker + Q8 codec)
| 场景 | 结果 |
|---|---|
冷启动到 /health 200 | ~10 s |
| 非流式 wav,4 s 音频 | 1.0~1.3 s(RTF 0.26~0.31) |
| pcm 流式首包 | 80~130 ms |
| pcm 流式,5~6 s 音频 | 1.0~1.8 s(RTF 0.21~0.30) |
is_streaming_capable = True 时宿主按「预缓冲 900 ms + 之后每 0.24 s 一块」播放, 比等整段 wav 起播更早。
参考实现
| 目录 | 说明 |
|---|---|
extensions/community_qwen3_tts/ | tts-server + 克隆音色持久化 + 三变体(完整范例) |
