Skip to content

本地 TTS 服务(tts_server_runtime) ​

想接 Qwen3-TTS(含 3 秒克隆音色 / CustomVoice / VoiceDesign)又不想自己写推理? 宿主自带一份 tts-server.exe(qwentts.cpp,MIT), 走 OpenAI 兼容 HTTP。插件只负责起进程 + 发请求 + 管音色。

运行库随安装包内置(bundled),用户不需要点「下载运行库」,也不需要联网。


1) 声明与取用 ​

json
{
  "requires_runtime": [
    { "id": "tts_server_runtime", "reason": "提供 tts-server 运行时(qwentts.cpp,随安装包内置)" }
  ]
}
python
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) 启动参数 ​

python
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 建议填一个稳定名字
--deviceCUDA0 / Vulkan0 / ROCm0 / MTL0 / CPU(不是 cuda / vulkan,见 §5)
--port建议 18xxx 高位端口:8080/8081 常年被 Steam 的 steamwebhelper 占用
--no-fa关 flash attention(排障用)
--codec-chunk-durwav 响应的 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 → 用于认领端口 ​

json
{"object":"list","data":[{"id":"Qwen3-TTS-1.7B","object":"model","owned_by":"local"}]}

id 就是你启动时给的 --alias。宿主机上可能同时跑着别的插件起的服务, 必须通过它确认「这个端口上的是我启动的那一个」,否则会把别人的服务当自己的复用。

3.3 POST /v1/audio/speech ​

json
{
  "model": "Qwen3-TTS-1.7B",
  "input": "要合成的文本",
  "voice": "音色名或留空",
  "response_format": "wav",
  "max_new_tokens": 400,
  "instructions": "(仅 voicedesign)年轻女性,温柔"
}
字段说明
input待合成文本
voicebase 变体:已注册的克隆音色名(传文件路径会被判 400 unknown voice);customvoice 变体:内置 speaker 名;留空 = 服务端默认音色
response_formatwav(整段一次性)/ 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/voicesbody {"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 文件能力
baseqwen-talker-1.7b-base-Q4_K_M.gguf参考音频克隆音色(无内置 speaker)
customvoiceqwen-talker-1.7b-customvoice-Q4_K_M.gguf内置预设音色,voice 直接选
voicedesignqwen-talker-1.7b-voicedesign-Q4_K_M.gguf用文字描述生成音色

三者共用 qwen-tokenizer-12hz-Q8_0.gguf。把每个变体声明成独立的 model_artifacts, 并在 model_artifacts() 里把当前生效的那份排第一位(见 模型工件)。


5) 计算设备:--device 取设备名,不是 cuda / vulkan ​

python
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 + 克隆音色持久化 + 三变体(完整范例)

相关 ​

TransBox - 赋能全人类无界限跨语言交流