跳到主要内容

WebSocket ​流式​(TTS)

WebSocket TTS API ​让​您​发送​完整​文本,​并​在​单个​连接​上​流式​接收生成​的​音频,​从而​在​合成​完成​之前​即可​开始​播放。

wss://api.shisa.ai/ws/tts/realtime
输入完整文本,流式输出音频

尽管​路​径​名​如此,​这不​是实时​ A​SR ​那样​的​输入​流式​ A​PI。​您​发送​一​个​完整​的​ tts.speak ​文本​请​求;​服务会​以​二​进制帧​或​ base64 JSON​ 块​的​形式​将​生成​的​音频​流式​返回。

如果​需要​一​次​性返​回​整​个​音频​文件​的​单次​请​求​/响应,​请​改用​ POST /tts ​端点

要求

  • 一​个​被​允许​使用​所​选​音色​的​ A​PI 密钥​(WebSocket TTS ​的​配额​和​速率​限制​在​ shisa/tts ​服务​下​管理)。
  • 来自​ GET /tts/voices ​的​音色​ UUID​——​参见音色​目录
  • 一​个​能够​设置​ Authorization ​标头​的​服务器​端 WebSocket ​客户端。​请​将​ A​PI ​密钥​保存在​服务​器端。

在​握手​期间,​使用​标准​的​ bearer ​令牌​进行​认证​:

Authorization: Bearer YOUR_API_KEY

工作​原理

  1. 使用​ Authorization ​标头​打​开​ WebSocket。
  2. 发送​一​条​包含​必需​的​ voice_id ​和​ format(以及​可选​的​ sample_ratetemperature ​和​ audio_transport)​的​ session.update
  3. 等​待 session.created
  4. 发送​一​个​包含​完整​文本​的​ tts.speak ​请​求。
  5. 读取 tts.audio.start、​音频帧、tts.audio.done ​和​ tts.usage
  6. 依次​发送​更​多​ tts.speak ​请​求,​或​关闭​套​接字。
一次只能进行一个合成

每​个​会话​一​次​只​能​有​一​个​进行​中​的​合成。​在​一​个​合成​仍​在​进行​时​发送​第二​个​ tts.speak 会​返​回 synthesis_in_progress ​错误。​对于​已​在​后端​开始​的​尝试,​请​在​收到​ tts.usage ​后​再​发送​下​一​个​请求;​对于​到​达​后​端​之前​被​拒绝​的​请求,​请​等​待​作为​终止​事件​的​ tts.error。​如果​代码​为​ backend_request_failed,​还​应​读取紧随其后​的​ tts.usage

客户​端​消息

session.update

在​合成​之前​发送​一​条 session.update:

{
"type": "session.update",
"id": "cfg_0001",
"session": {
"voice_id": "61ba1141-60aa-4bc3-a3b3-be1ec20700b3",
"format": "mp3",
"audio_transport": "binary"
}
}
字段是否​必​需备​注
id可​选客户​端关联 ​ID,​在​配置​错误时​回显。
voice_id必​需来自​ GET /tts/voices ​的​公开​音色​ UU​ID。
format必​需输出音频格式​——mp3wavoggpcm ​或​ flac。​该​格式​必须​受​所​选​音色​支持,​并且​可​在​对应​提供​商上​流式​传输。
sample_rate可​选省略​或​设为0则​使用​后​端默​认值​(24000 Hz)。​仅​使用​所​选​音色​支持​的​可配​置值。​Qwen 后​端​音色​使用ogg时​不​接受​覆​盖值。
temperature可​选提供​商​专属​的​语音​变化​控制,​目前​由​ Qwen 后​端音色​接受。​其他​音色​以及​使用​默​认值​时​请​省略;​显式​的0.0会​作为​实值​发送。
audio_transport可​选binary(默认)​或​ base64_json。​参见音频​传输
格式特定的流式支持

音色​目录中​的streaming: true是​必要​条件,​但​不​表示formats中​的​每​个​值​都​可​流式​传输。​由于​此​端点始终​流式​输出,​不​受​支持​的​提供​商/格式​组合会​返​回tts.error,​其code"unsupported_streaming_format"

tts.speak

发送​要​合成​的​完整​文本​:

{
"type": "tts.speak",
"id": "utt_0001",
"text": "こんにちは。WebSocket TTS のテストです。"
}
字段是否​必​需备​注
id可​选客户​端关联 ​ID,​在​此​请​求​的​ TTS 事件​中​回显。
text必​需要​合成​的​完整​文本。​默认上​限​为​ 5000 ​个​字符。

服务​事件

session.created

会话​已​配置​完成,​可以​进行​ tts.speak:

{
"type": "session.created",
"session": {
"id": "router-session-request-id",
"service": "shisa/tts",
"voice_id": "61ba1141-60aa-4bc3-a3b3-be1ec20700b3",
"format": "mp3",
"sample_rate": 24000,
"model": "speech-2.8-hd"
}
}

tts.audio.start

标记​一​个​被​接受​的​合成​请​求,​并​描述​您​将​收到​的​音频​:

{
"type": "tts.audio.start",
"id": "utt_0001",
"request_id": "synthesis-request-id",
"format": "mp3",
"content_type": "audio/mpeg",
"sample_rate": 24000,
"voice_id": "61ba1141-60aa-4bc3-a3b3-be1ec20700b3",
"model": "speech-2.8-hd",
"audio_transport": "binary"
}

音频​传输

音​频​字节​的​到达​方式​取决于​您​在​ session.update ​中​设置​的​ audio_transport:

  • binary(默认)​——生成​的​音频​以​原始​二​进制 WebSocket​ 帧​的​形式​到达。​请​按​顺序连接​它们。
  • base64_json——​音频​以​ tts.audio.delta JSON ​消息​的​形式​到​达,​base64 ​编码​的​字节​位​于​ audio ​中,​并​带​有​递增​的​ seq:
{
"type": "tts.audio.delta",
"id": "utt_0001",
"request_id": "synthesis-request-id",
"seq": 1,
"audio": "<base64-audio-bytes>"
}

tts.audio.done

此​请求​的​合成​已​完成​:

{
"type": "tts.audio.done",
"id": "utt_0001",
"request_id": "synthesis-request-id",
"audio_bytes": 12345,
"chunks": 4
}

tts.usage

已​在​后端​开始​的​合成​尝试​的​最终​用量。​成功​时,​该​事件​会​在​ tts.audio.done ​之后​发送​:

{
"type": "tts.usage",
"request_id": "synthesis-request-id",
"session_id": "router-session-request-id",
"client_id": "utt_0001",
"model": "speech-2.8-hd",
"usage": {
"input_chars": 24,
"input_bytes": 72,
"audio_bytes": 12345,
"status": "completed",
"final": true
}
}

如果​后​端​请​求​失败,​路由​器​会​先​发送​代码​为​ backend_request_failed ​的​ tts.error,​随后​发送​ tts.usage。​未返​回​音频​时,usage.status ​为​ backend_error;​已​返回部分​音频​时,​则​为​ partial_backend_error

对于​到​达​后端​的​尝试​(包括​失败​的​尝试),​路由​器​会​创建​用量​记录;​只要​连接​仍​可写,​也​会​发送​ tts.usage。​在​输入​验证、​访问​控制、​速率​限制​或​后端选择​阶段​被​拒绝​的​请求​只返​回 tts.error,​不​会​创建​用量​记录。​因此,​并​非​每​个​ tts.error 后​都​会​跟​随 tts.usage

tts.error ​与​ router.error

在​ WebSocket ​升级​之前​发生​的​失败​会​返回​结构​化 HTTP ​错误,​而​不​是​ WebSocket​ ​事件。​常见​代码​包括​ invalid_websocket_upgradeauth_token_requiredinvalid_token_formatinvalid_tokenlegacy_api_key_rejectedws_origin_deniedunknown_servicews_connection_limit_exceededws_connection_quota_unavailable ​和​ websocket_draining

TTS 配置、​发声​和​后端错误​采用​以下​格式。​若客户​端关联 ​I​D ​可用,id 会​将​其​回​显:

{
"type": "tts.error",
"id": "utt_0001",
"code": "backend_request_failed",
"message": "Backend TTS request failed"
}
代​码含义
invalid_message, invalid_id, id_too_long, invalid_session已​识别​消息、​关联 ​ID ​或​路由​器​会话​状态​无效。
session_already_configured此​连接​上​的​ session.update ​已​成功​处理。
missing_voice_id, unknown_voice_id, tts_service_unavailable, service_access_denied音色​选择、​服务​可​用性​或​访问权限​无效。
unsupported_audio_format, unsupported_sample_rate, unsupported_audio_transport, unsupported_streaming_format所​选​音色​或​后端​不​支持​请​求​的​输出​设置。
session_not_configured, empty_text, tts_text_too_long, synthesis_in_progresstts.speak ​的​状态​或​文本​无效。
rate_limited, service_rate_limited超出​ ​API 密钥​或​ TTS ​服务​的​速率​限制。
rate_limiter_unavailable, service_rate_limiter_unavailable速率​限制​服务暂时​不​可用。
backend_unavailable当前​没有​可用​的​ TTS ​后端。​后端​请​求尚未​开始,​因此​不​会​创建​用量​记录。
backend_request_failed合成​已​在​后端​开始​但​失败。​此​错误​之后​会​发送​最终​的​ tts.usage
tts_control_frame_too_largeJSON ​控制​消息​超过​配置​的​字节​上限。​若读取​上限先​检测​到​该​问题,​连接会​以​代码​ 1009 ​关闭。

在​消息​类型​得到​处理​之前​检测到​的​控制​错误​使用​ type: "router.error",​代码​包括​ invalid_jsonmissing_message_type ​和​ unknown_message_type。​客户​端​发送​二​进制帧会​使​连接​以​ WebSocket 代​码​ 1003 ​关闭;​控制​帧​过​大时​则​以​代码​ 1009 ​关闭。

最​小​ Python 客户​端

安装​依赖​:

python -m pip install websockets

设置​密钥​并​运行​:

export SHISA_API_KEY="shsk:..."
export TTS_WS_URL="wss://api.shisa.ai/ws/tts/realtime"
export TTS_VOICE_ID="61ba1141-60aa-4bc3-a3b3-be1ec20700b3"
python websocket_tts.py > output.mp3

websocket_tts.py:

#!/usr/bin/env python3
import asyncio
import json
import os
import sys

import websockets


async def main() -> None:
url = os.environ.get("TTS_WS_URL", "wss://api.shisa.ai/ws/tts/realtime")
api_key = os.environ["SHISA_API_KEY"]
voice_id = os.environ["TTS_VOICE_ID"]
text = os.environ.get("TTS_TEXT", "こんにちは。WebSocket TTS のテストです。")
audio_format = os.environ.get("TTS_FORMAT", "mp3")

headers = [("Authorization", f"Bearer {api_key}")]
audio = bytearray()

async with websockets.connect(url, additional_headers=headers, max_size=None) as ws:
await ws.send(
json.dumps(
{
"type": "session.update",
"id": "cfg_0001",
"session": {
"voice_id": voice_id,
"format": audio_format,
"audio_transport": "binary",
},
}
)
)

# 在发话之前等待 session.created。
while True:
event = json.loads(await ws.recv())
if event.get("type") == "session.created":
break
if event.get("type") in {"tts.error", "router.error", "error"}:
raise RuntimeError(event)

await ws.send(json.dumps({"type": "tts.speak", "id": "utt_0001", "text": text}))

# 二进制帧是音频,JSON 帧是事件。
backend_error = None
while True:
msg = await ws.recv()
if isinstance(msg, bytes):
audio.extend(msg)
continue
event = json.loads(msg)
event_type = event.get("type")
if event_type == "tts.error":
if event.get("code") == "backend_request_failed":
# 后端失败后会发送最终用量。
backend_error = event
continue
raise RuntimeError(event)
if event_type == "tts.usage":
print(json.dumps(event, ensure_ascii=False), file=sys.stderr)
if backend_error is not None:
raise RuntimeError(backend_error)
break
if event_type in {"router.error", "error"}:
raise RuntimeError(event)

sys.stdout.buffer.write(audio)


if __name__ == "__main__":
asyncio.run(main())

后续​步​骤