tts — 语音合成
接云端 TTS 或本地合成引擎。当前引擎互斥。支持音色表 / 克隆音色 / 流式 / 语速。
声明
json
{
"id": "myteam.tts.my_voice",
"name": "My Voice TTS",
"version": "1.0.0",
"target_app_version": "0.2",
"type": "tts",
"entrypoint": "my_tts.py:MyTTSProvider",
"tags": ["cloud"],
"requires_hf": false,
"capabilities": {
"voices": true,
"voice_clone": true,
"voice_remove": true,
"speed": false
},
"config_schema": [
{ "key": "api_key", "label": "API Key", "type": "password", "is_secret": true }
]
}capabilities 的含义与「声明式 vs 类属性」两种等价写法见 能力声明与只读契约。
最小实现
python
import numpy as np
from transbox.plugins.base import BaseTTSProvider
class CustomTTSProvider(BaseTTSProvider):
supports_voice_clone = True # True → 设置页出现「注册克隆音色」面板(含软件内录制)
supports_speed = False # False → 宿主统一做保音高变速,你什么都不用做
@property
def sample_rate(self) -> int:
return 24000
@property
def is_streaming_capable(self) -> bool:
return True
def synthesize(self, text, voice_id="", voice=None, language="zh"):
# 返回 (float32 单声道 PCM, 采样率)
return np.zeros(24000, dtype=np.float32), self.sample_rate
def stream_tts(self, text, voice_id="", voice=None, language="zh"):
"""可选:按块 yield(宿主按预缓冲逐块播,起播更早)。
未实现时宿主退化为整段 synthesize。"""
yield self.synthesize(text, voice_id, voice, language)[0]参数约定:
voice优先于voice_id(宿主可能只传其中一个)。- 返回
(float32 单声道 PCM, 采样率)。 - 音色配置键:宿主把用户选中的音色写进插件配置的
voice键 (分语种时还有voice_en/voice_other,缺省回落voice)。 实现里读self._config.get("voice")即可。
音色表
python
def list_voices(self) -> list[dict]:
return [
{"id": "vivian", "name": "薇薇安", "lang": "zh", "kind": "speaker"},
{"id": "my-clone", "name": "我的音色", "lang": "zh", "kind": "registered"},
]kind="speaker"= 模型自带音色;kind="registered"= 用户注册的克隆音色。- 只有
kind="registered"的音色才会显示「删除音色」。 - 只返回真实可用的音色(例:
base变体没有内置 speaker,就只返回已注册的克隆音色)。 - 只读方法:服务未启动时返回已缓存/本地保存的音色表或空表, 绝不为了列音色而拉起服务(见 能力声明)。
克隆音色
python
def register_voice(self, voice_name, audio_samples, reference_text="", sample_rate=24000) -> bool:
"""audio_samples: float32 单声道 numpy,1~30 秒。"""
...
def remove_voice(self, voice_name: str) -> bool:
...- 参考音频由宿主提供:用户选文件,或者在设置页内直接录制 (宿主负责麦克风采集、写 WAV、读回 float32)。 插件不需要碰麦克风、文件对话框或音频格式转换。
- 你只负责转码 / 上传 / 落盘,返回
True/False。
克隆音色的持久化(强制)
若你的音色只注册在服务端内存里(进程重启即丢失),必须自己持久化:
- 把参考音频与
ref_text存到插件私有数据目录(self.get_data_dir()); - 在服务就绪后自动补注册:对比服务端
GET /v1/audio/voices,缺哪条补哪条; 同一服务实例只跑一次(服务换实例 / 重启 / 换设备 / 换端口后重跑); remove_voice()同步删本地;list_voices()在服务未启动时返回本地保存的音色(kind="registered"),不得拉起服务。
否则用户每次重启应用都得重新注册音色。实测参考实现: extensions/community_qwen3_tts(_persist_voice / _restore_voices / _read_voice_index)。
语速
| 情况 | 宿主行为 | 你要做什么 |
|---|---|---|
supports_speed = False(默认) | 合成后统一做保音高时间伸缩(宿主自带 DSP) | 什么都不用做 |
supports_speed = True | 把 speed=(1.0 = 原速,范围 0.5~2.0)传给你 | 自己在模型内实现 |
流式 × 语速:speed != 1.0 且插件未声明 supports_speed 时,宿主不会调用 stream_tts(整段才好做时间伸缩)。
多能力模型(变体)
同一模型仓库里有多个互斥变体时(例:base 克隆音色 / customvoice 内置音色 / voicedesign 文字描述音色),把它们声明成多个 model_artifacts,并在 model_artifacts() 里把当前生效的那份排到第一位——宿主模型卡只展示/下载第一个同 category 的工件, 用户不会被逼把每个 ~1.1 GB 的变体都下下来,也不会被误报「未就绪」。
python
def model_artifacts(self) -> list[dict]:
arts = [...] # 你的全部变体
active = self._resolve_variant() # 按配置 / 已下载情况选出生效的
arts.sort(key=lambda a: 0 if a["id"] == active else 1)
return arts见 模型工件 § 多能力模型。
本地服务型 TTS(推荐:audio.cpp 与 BaseLocalTTSProvider)
自己起本地服务或使用 GGUF 语音模型的,推荐结合宿主运行库与标准基类开发:
- 声明依赖宿主运行库(
mod.json):json{ "requires_runtime": [ { "id": "audiocpp_runtime", "reason": "提供 audio.cpp 本地推理运行时(CUDA 13 / Vulkan / CPU)" } ] }接 Qwen3-TTS 用另一份内置运行库:
tts_server_runtime(tts-server.exe, OpenAI 兼容/v1/audio/speech+ 克隆音色注册表), 启动参数与 HTTP 契约见 本地 TTS 服务。 - 继承
BaseLocalTTSProvider:- 绝不在
initialize()启动服务,ensure_loaded()或合成入口才按需启动; - 自动获得 GPU/CPU 设备热切(CUDA 13 / Vulkan / CPU)、端口冲突清理与 Windows JobObject 进程/显存彻底回收;
- 完整参考代码见 本地服务型引擎强制契约 § 示例 2。
- 绝不在
- 轻量纯 CPU 推理方案:
- 若模型结构为标准 VITS/ONNX(如 MeloTTS),可通过
sherpa-onnx在进程内加载运行,无需拉起外部服务进程。
- 若模型结构为标准 VITS/ONNX(如 MeloTTS),可通过
参考实现
| 目录 | 类型 |
|---|---|
extensions/community_cloud_mimo_tts/ | 云端 API |
extensions/workshop_openai_tts/ | OpenAI 兼容(kokoro-fastapi / GPT-SoVITS / fish-speech…) |
extensions/community_qwen3_tts/ | 本地服务型 + 克隆音色 + 变体(完整范例) |
