Messages
{
"event": "<string>",
"model": "<string>",
"voice_setting": {
"voice_id": "<string>",
"speed": 123,
"vol": 123,
"pitch": 123,
"latex_read": true
},
"audio_setting": {
"sample_rate": 123,
"bitrate": 123,
"format": "<string>",
"channel": 123
}
}{
"event": "<string>",
"text": "<string>",
"dictionary": {
"original": "<string>",
"replacement": "<string>"
}
}{
"event": "<string>"
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}语音合成
语音合成 WebSocket (TTS)
基于 WebSocket 的实时 TTS 合成协议参考
WSS
/
ws
/
v1
/
t2a_v2
说明
基于 WebSocket 的实时文本到语音合成接口,支持增量式文本输入与流式音频输出,适用于实时对话、在线客服、边生成边播报等低延迟场景。单次连接支持多次合成任务,最大文本长度 10000 字符。- 接入地址:
wss://api.senseaudio.cn/ws/v1/t2a_v2 - 鉴权方式:Bearer Token,格式
Authorization: Bearer SENSEAUDIO_API_KEY - 消息格式:
application/json,音频仅返回 hex 编码字符串 - 计费单位:按合成字符数计费,详见 计费说明
- 音色入参:
voice_id必须为当前账号可用音色,参考 音色列表 - 标准 HTTP 合成:参考 语音合成 HTTP
- SSE 合成:参考 语音合成 HTTP 流式
- WebSocket 接口只支持返回 hex 格式 的音频数据。
- 当最后一次收到服务端返回结果后超过 120 秒 没有发送新事件时,WebSocket 连接自动断开。
- 音色模型名称:senseaudio-tts-1.5-260319。
请求头 (Request Headers)
| 参数名 | 必填 | 说明 | 示例 |
|---|---|---|---|
| Authorization | 是 | 鉴权 Token。格式:Bearer SENSEAUDIO_API_KEY | Bearer sk-123456… |
| Content-Type | 是 | 内容类型。固定为 application/json | application/json |
通信流程
1. 客户端建立 WebSocket 连接
↓
2. 服务端返回 connected_success 事件
↓
3. 客户端发送 task_start 事件(包含音色、音频格式等配置)
↓
4. 服务端返回 task_started 事件
↓
5. 客户端发送 task_continue 事件(发送待合成文本)
↓
6. 服务端返回音频数据(可多次发送 task_continue)
↓
7. 客户端发送 task_finish 事件
↓
8. 服务端返回 task_finished 事件并关闭连接
客户端事件
1. task_start - 开始任务
发送此事件正式开始合成任务。当服务端返回 task_started 事件时,标志着任务已成功开始。只有在接收到该事件后,才能向服务器发送 task_continue 或 task_finish 事件。 请求参数| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| event | string | 是 | 无 | 固定值:task_start |
| model | string | 是 | 无 | 模型名称,示例值:senseaudio-tts-1.5-260319 |
| voice_setting | object | 是 | 无 | 声音设置 |
| voice_setting.voice_id | string | 是 | 无 | 主音色名称 |
| voice_setting.speed | float | 否 | 1.0 | 语速,取值范围 [0.5, 2.0] |
| voice_setting.vol | float | 否 | 1.0 | 音量,取值范围 [0.01, 10.0] |
| voice_setting.pitch | int | 否 | 0 | 音调,取值范围 [-12, 12],0 表示保持原始音调 |
| voice_setting.latex_read | boolean | 否 | false | 数学公式朗读,支持 LaTeX、MathML、Unicode 数学符号等格式。(会产生额外的性能损耗) |
| audio_setting | object | 否 | 无 | 音频格式设置 |
| audio_setting.sample_rate | int | 否 | 32000 | 音频采样率,取值范围 [8000, 16000, 22050, 24000, 32000, 44100] |
| audio_setting.bitrate | int | 否 | 128000 | 音频码率,取值范围 [32000, 64000, 128000, 256000] |
| audio_setting.format | string | 否 | mp3 | 输出格式:mp3、wav、pcm、flac |
| audio_setting.channel | int | 否 | 2 | 音频声道,1:单声道,2:双声道 |
{
"event": "task_start",
"model": "senseaudio-tts-1.5-260319",
"voice_setting": {
"voice_id": "male_0004_a",
"speed": 1,
"vol": 1,
"pitch": 0,
"latex_read": false
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
}
}
| 参数名 | 类型 | 说明 |
|---|---|---|
| session_id | string | 会话 ID |
| event | string | 事件类型 |
| trace_id | string | 链路追踪 ID |
| base_resp | object | 请求状态信息 |
| base_resp.status_code | integer | 状态码 |
| base_resp.status_msg | string | 状态详情 |
{
"session_id": "69c20e38c8761996a85d57881fe4d817",
"event": "task_started",
"trace_id": "69c20e38c8761996a85d57881fe4d817",
"base_resp": { "status_code": 0, "status_msg": "success" }
}
2. task_continue - 任务继续
当收到服务端返回的 task_started 事件后,任务正式开始,可通过发送 task_continue 事件发送要合成的文本。支持顺序发送多个 task_continue 事件,实现分段文本合成。 请求参数| 参数名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| event | string | 是 | 固定值:task_continue | |
| text | string | 是 | 合成文本内容,支持中英文;支持 <break> 停顿符,详见下方停顿符说明 | |
| dictionary | array | 否 | 多音字配置列表(模型必须为 senseaudio-tts-1.5-260319) | [{"original": "好干净","replacement": "[hao4]干净"}] |
LaTeX 公式说明
控制是否朗读 latex 公式,默认为 false。- 请求中的公式需要在公式的首尾加上
$$。 - 请求中公式若有
\,需转义成\\。

$$E_k = \\frac{1}{2} m v^2$$
{
"event": "task_continue",
"text": "$$E_k = \\frac{1}{2} m v^2$$"
}
<break> 停顿符说明
<break> 用于在语音合成中插入停顿。
<break time=500>
- time 单位为毫秒(ms)
- 500 表示停顿 500 毫秒
- 最小值为 100 毫秒,最大值无限制
{
"event": "task_continue",
"text": "你好,这是来自 <break time=5000> SenseAudio 的第一条语音"
}
dictionary (多音字纠正)
| 参数名 | 类型 | 必填 | 描述 | 默认值 | 示例 |
|---|---|---|---|---|---|
| original | string | 是 | 原始文本。 | 无 | 好干净 |
| replacement | string | 是 | 多音字配置。 | 无 | [hao4]干净 |
{
"event": "task_continue",
"text": "好干净",
"dictionary": [
{ "original": "好干净", "replacement": "[hao4]干净" }
]
}
| 参数名 | 类型 | 说明 |
|---|---|---|
| session_id | string | 会话 ID |
| event | string | 事件类型 |
| trace_id | string | 链路追踪 ID |
| is_final | boolean | 当前 task_continue 语音合成任务是否结束 |
| base_resp | object | 请求状态信息 |
| base_resp.status_code | integer | 状态码 |
| base_resp.status_msg | string | 状态详情 |
| data | object | 返回的合成数据对象(可能为 null) |
| data.audio | string | 合成后的音频数据(hex 编码) |
| data.status | integer | 音频流状态:1 表示合成中,2 表示合成结束 |
| extra_info | object | 音频附加信息(流式返回时只有最后一个 chunk 会返回) |
| extra_info.audio_length | integer | 音频时长(毫秒) |
| extra_info.audio_sample_rate | integer | 音频采样率 |
| extra_info.audio_size | integer | 音频文件大小(字节) |
| extra_info.bitrate | integer | 音频比特率 |
| extra_info.audio_format | string | 音频格式,取值范围 [mp3, pcm, flac, wav] |
| extra_info.audio_channel | integer | 音频声道数,1:单声道,2:双声道 |
| extra_info.word_count | integer | 字数统计(按字素簇 grapheme cluster 统计,排除纯空白/标点/控制符) |
| extra_info.usage_characters | integer | 字符数统计(按 Unicode 码点统计) |
{
"session_id": "69c20e38c8761996a85d57881fe4d817",
"event": "task_continued",
"trace_id": "69c20e38c8761996a85d57881fe4d817",
"is_final": false,
"data": { "audio": "hex编码的音频数据...", "status": 1 },
"base_resp": { "status_code": 0, "status_msg": "success" }
}
3. task_finish - 结束任务
服务端收到此事件后,会等待当前队列中所有合成任务完成后,关闭 WebSocket 连接并结束任务。 请求参数| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| event | string | 是 | 固定值:task_finish |
{ "event": "task_finish" }
{
"session_id": "69c20e38c8761996a85d57881fe4d817",
"event": "task_finished",
"trace_id": "69c20e38c8761996a85d57881fe4d817",
"base_resp": { "status_code": 0, "status_msg": "success" }
}
服务端事件
connected_success - 连接建立成功
初次请求接口时,表示 WebSocket 连接建立成功。{
"session_id": "xxxx",
"event": "connected_success",
"trace_id": "xxx",
"base_resp": { "status_code": 0, "status_msg": "success" }
}
task_started - 任务已开始
标志任务已成功开始,客户端可以开始发送 task_continue 事件。{
"session_id": "xxxx",
"event": "task_started",
"trace_id": "xxxxx",
"base_resp": { "status_code": 0, "status_msg": "success" }
}
task_finished - 任务已结束
标志任务已结束,WebSocket 连接即将关闭。{
"session_id": "xxxx",
"event": "task_finished",
"trace_id": "xxxx",
"base_resp": { "status_code": 0, "status_msg": "success" }
}
task_failed - 任务失败
标志任务失败,base_resp.status_msg 中包含错误信息。
{
"session_id": "xxxx",
"event": "task_failed",
"trace_id": "xxxxx",
"base_resp": { "status_code": 1004, "status_msg": "具体错误信息" }
}
使用示例
- Node.js
- Python
- Go
- Java
依赖:npm install ws
const fs = require('fs')
const WebSocket = require('ws')
const WS_URL = 'wss://api.senseaudio.cn/ws/v1/t2a_v2'
const API_KEY = process.env.SENSEAUDIO_API_KEY
if (!API_KEY) throw new Error('Missing env: SENSEAUDIO_API_KEY')
const output = fs.createWriteStream('output.mp3')
const ws = new WebSocket(WS_URL, {
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
})
ws.on('open', () => console.log('WebSocket 连接已建立'))
ws.on('message', (data) => {
const response = JSON.parse(data.toString())
console.log('收到服务端事件:', response.event)
if (response.event === 'connected_success') {
ws.send(JSON.stringify({
event: 'task_start',
model: 'senseaudio-tts-1.5-260319',
voice_setting: { voice_id: 'male_0004_a', speed: 1.0, vol: 1.0, pitch: 0, latex_read: false },
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3', channel: 1 },
}))
return
}
if (response.event === 'task_started') {
ws.send(JSON.stringify({ event: 'task_continue', text: '道可道,非常道。名可名,非常名。无名天地之始,有名万物之母。' }))
ws.send(JSON.stringify({ event: 'task_continue', text: '故常无欲,以观其妙;常有欲,以观其徼。' }))
ws.send(JSON.stringify({ event: 'task_finish' }))
return
}
if (response?.data?.audio) {
output.write(Buffer.from(response.data.audio, 'hex'))
}
if (response.event === 'task_finished') { output.end(); ws.close() }
if (response.event === 'task_failed') {
console.error('任务失败:', response?.base_resp?.status_msg)
output.end(); ws.close()
}
})
ws.on('error', (err) => console.error('WebSocket 错误:', err))
ws.on('close', () => console.log('WebSocket 连接已关闭'))
依赖:pip install websocket-client
import json
import os
import websocket
WS_URL = "wss://api.senseaudio.cn/ws/v1/t2a_v2"
API_KEY = os.getenv("SENSEAUDIO_API_KEY")
if not API_KEY:
raise RuntimeError("Missing env: SENSEAUDIO_API_KEY")
output_file = open("output.mp3", "wb")
def on_open(ws):
print("WebSocket 连接已建立")
def on_message(ws, message: str):
resp = json.loads(message)
event = resp.get("event")
print("收到服务端事件:", event)
if event == "connected_success":
ws.send(json.dumps({
"event": "task_start",
"model": "senseaudio-tts-1.5-260319",
"voice_setting": {"voice_id": "male_0004_a", "speed": 1.0, "vol": 1.0, "pitch": 0, "latex_read": False},
"audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1}
}))
return
if event == "task_started":
ws.send(json.dumps({"event": "task_continue", "text": "道可道,非常道。名可名,非常名。无名天地之始,有名万物之母。"}))
ws.send(json.dumps({"event": "task_continue", "text": "故常无欲,以观其妙;常有欲,以观其徼。"}))
ws.send(json.dumps({"event": "task_finish"}))
return
data = resp.get("data") or {}
audio_hex = data.get("audio")
if audio_hex:
output_file.write(bytes.fromhex(audio_hex))
if event in ("task_finished", "task_failed"):
if event == "task_failed":
print("任务失败:", (resp.get("base_resp") or {}).get("status_msg"))
output_file.close(); ws.close()
def on_error(ws, err): print("WebSocket 错误:", err)
def on_close(ws, status_code, msg): print("WebSocket 连接已关闭:", status_code, msg)
ws = websocket.WebSocketApp(
WS_URL,
header=[f"Authorization: Bearer {API_KEY}", "Content-Type: application/json"],
on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close,
)
ws.run_forever()
依赖:go get github.com/gorilla/websocket
package main
import (
"encoding/hex"
"encoding/json"
"log"
"net/http"
"os"
"github.com/gorilla/websocket"
)
const wsURL = "wss://api.senseaudio.cn/ws/v1/t2a_v2"
func mustMarshal(v any) []byte { b, err := json.Marshal(v); if err != nil { panic(err) }; return b }
func main() {
apiKey := os.Getenv("SENSEAUDIO_API_KEY")
if apiKey == "" { log.Fatal("Missing env: SENSEAUDIO_API_KEY") }
header := http.Header{}
header.Set("Authorization", "Bearer "+apiKey)
header.Set("Content-Type", "application/json")
c, _, err := websocket.DefaultDialer.Dial(wsURL, header)
if err != nil { log.Fatal("dial:", err) }
defer c.Close()
out, err := os.Create("output.mp3")
if err != nil { log.Fatal(err) }
defer out.Close()
for {
_, msg, err := c.ReadMessage()
if err != nil { log.Fatal("read:", err) }
var resp map[string]any
if err := json.Unmarshal(msg, &resp); err != nil { log.Fatal(err) }
event, _ := resp["event"].(string)
log.Println("收到服务端事件:", event)
switch event {
case "connected_success":
_ = c.WriteMessage(websocket.TextMessage, mustMarshal(map[string]any{
"event": "task_start",
"model": "senseaudio-tts-1.5-260319",
"voice_setting": map[string]any{"voice_id": "male_0004_a", "speed": 1.0, "vol": 1.0, "pitch": 0, "latex_read": false},
"audio_setting": map[string]any{"sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1},
}))
case "task_started":
_ = c.WriteMessage(websocket.TextMessage, mustMarshal(map[string]any{"event": "task_continue", "text": "道可道,非常道。名可名,非常名。无名天地之始,有名万物之母。"}))
_ = c.WriteMessage(websocket.TextMessage, mustMarshal(map[string]any{"event": "task_continue", "text": "故常无欲,以观其妙;常有欲,以观其徼。"}))
_ = c.WriteMessage(websocket.TextMessage, mustMarshal(map[string]any{"event": "task_finish"}))
case "task_failed", "task_finished":
if event == "task_failed" {
if baseResp, ok := resp["base_resp"].(map[string]any); ok {
if msg, ok := baseResp["status_msg"].(string); ok { log.Println("任务失败:", msg) }
}
}
return
}
if data, ok := resp["data"].(map[string]any); ok {
if audioHex, ok := data["audio"].(string); ok && audioHex != "" {
b, err := hex.DecodeString(audioHex)
if err != nil { log.Fatal(err) }
if _, err := out.Write(b); err != nil { log.Fatal(err) }
}
}
}
}
依赖:OkHttp + Jackson
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import okio.ByteString;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;
public class SenseAudioTtsWsExample {
private static final String WS_URL = "wss://api.senseaudio.cn/ws/v1/t2a_v2";
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("SENSEAUDIO_API_KEY");
if (apiKey == null || apiKey.isEmpty()) throw new RuntimeException("Missing env: SENSEAUDIO_API_KEY");
OkHttpClient client = new OkHttpClient();
ObjectMapper mapper = new ObjectMapper();
Request request = new Request.Builder()
.url(WS_URL)
.addHeader("Authorization", "Bearer " + apiKey)
.addHeader("Content-Type", "application/json")
.build();
FileOutputStream out = new FileOutputStream("output.mp3");
client.newWebSocket(request, new WebSocketListener() {
@Override public void onOpen(WebSocket webSocket, Response response) { System.out.println("WebSocket 连接已建立"); }
@Override public void onMessage(WebSocket webSocket, String text) {
try {
JsonNode resp = mapper.readTree(text);
String event = resp.path("event").asText();
System.out.println("收到服务端事件: " + event);
if ("connected_success".equals(event)) {
Map<String, Object> payload = new HashMap<>();
payload.put("event", "task_start");
payload.put("model", "senseaudio-tts-1.5-260319");
Map<String, Object> voice = new HashMap<>();
voice.put("voice_id", "male_0004_a"); voice.put("speed", 1.0); voice.put("vol", 1.0);
voice.put("pitch", 0); voice.put("latex_read", false);
payload.put("voice_setting", voice);
Map<String, Object> audio = new HashMap<>();
audio.put("sample_rate", 32000); audio.put("bitrate", 128000);
audio.put("format", "mp3"); audio.put("channel", 1);
payload.put("audio_setting", audio);
webSocket.send(mapper.writeValueAsString(payload));
return;
}
if ("task_started".equals(event)) {
webSocket.send(mapper.writeValueAsString(Map.of("event", "task_continue", "text", "道可道,非常道。名可名,非常名。无名天地之始,有名万物之母。")));
webSocket.send(mapper.writeValueAsString(Map.of("event", "task_continue", "text", "故常无欲,以观其妙;常有欲,以观其徼。")));
webSocket.send(mapper.writeValueAsString(Map.of("event", "task_finish")));
return;
}
JsonNode audioHex = resp.path("data").path("audio");
if (!audioHex.isMissingNode() && !audioHex.isNull()) {
byte[] bytes = ByteString.decodeHex(audioHex.asText()).toByteArray();
out.write(bytes);
}
if ("task_finished".equals(event) || "task_failed".equals(event)) {
if ("task_failed".equals(event)) System.err.println("任务失败: " + resp.path("base_resp").path("status_msg").asText());
out.close(); webSocket.close(1000, "done");
}
} catch (Exception e) { e.printStackTrace(); webSocket.close(1001, "error"); }
}
@Override public void onFailure(WebSocket webSocket, Throwable t, Response response) {
t.printStackTrace();
try { out.close(); } catch (Exception ignored) {}
}
});
Thread.sleep(60_000);
client.dispatcher().executorService().shutdown();
}
}
技术规格
- 模型名称:senseaudio-tts-1.5-260319
- 最大文本长度:10000 字符
- 连接超时:最后一次收到服务端返回后 120 秒无新事件时自动断开
音频参数范围
| 参数 | 取值范围 | 默认值 | 说明 |
|---|---|---|---|
| 语速 | [0.5, 2.0] | 1.0 | 数值越大语速越快 |
| 音量 | [0.01, 10.0] | 1.0 | 数值越大音量越大 |
| 音调 | [-12, 12] | 0 | 正值提高,负值降低 |
| 采样率 | 8000, 16000, 22050, 24000, 32000, 44100 (Hz) | 32000 | 推荐 32000 |
| 码率 | 32000, 64000, 128000, 256000 (bps) | 128000 | 仅 MP3 格式 |
| 声道 | 1 (单声道), 2 (双声道) | 2 | - |
支持的音频格式
- MP3:推荐,压缩率高,兼容性好。
- WAV:无损音质,文件较大。
- PCM:原始音频数据。
- FLAC:无损压缩。
错误码说明
| 状态码 | 说明 | 解决方案 |
|---|---|---|
| 0 | 成功 | - |
| 1001 | 参数错误 | 检查请求参数格式和取值范围 |
注意事项
- 音频数据格式:WebSocket 接口只支持返回 hex 编码的音频数据,需要在客户端将 hex 字符串转换为二进制数据;音频格式由
audio_setting.format决定。 - 连接超时机制:最后一次收到服务端返回后 120 秒内无新事件发送时连接自动断开;建议在任务完成后主动发送
task_finish,长时间无操作时可发送心跳保持连接。 - 事件发送顺序:必须按照
task_start → task_continue → task_finish顺序发送;只有在收到task_started后才能发送task_continue;可以发送多个task_continue事件。
相关资源
语音合成 HTTP
标准 HTTP 合成接口参数详解。
语音合成 HTTP 流式
流式 HTTP 合成接口参数详解。
语音合成介绍
TTS 核心特性与音色生态。
音色列表
系统音色清单与
voice_id 规则。Messages
{
"event": "<string>",
"model": "<string>",
"voice_setting": {
"voice_id": "<string>",
"speed": 123,
"vol": 123,
"pitch": 123,
"latex_read": true
},
"audio_setting": {
"sample_rate": 123,
"bitrate": 123,
"format": "<string>",
"channel": 123
}
}{
"event": "<string>",
"text": "<string>",
"dictionary": {
"original": "<string>",
"replacement": "<string>"
}
}{
"event": "<string>"
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}{
"session_id": "<string>",
"event": "<string>",
"trace_id": "<string>",
"base_resp": {
"status_code": 123,
"status_msg": "<string>"
}
}bearerAuth
type:http
Bearer Token 鉴权,格式 Authorization: Bearer <SENSEAUDIO_API_KEY>
task_start
type:object
开始合成任务
task_continue
type:object
发送待合成文本
task_finish
type:object
结束合成任务
connected_success
type:object
连接建立成功
task_started
type:object
任务已开始
task_continued
音频分片返回
task_finished
type:object
任务已结束
task_failed
type:object
任务失败
⌘I