---
url: https://limapi.com/docs/guide/realtime-tts.md
---
# 实时语音合成（WebSocket）

```
wss://limapi.com/api/v1/realtime/audio/speech
```

流式实时语音合成接口：文本边输入、音频边输出，支持**连接复用**（同一连接串行合成多个任务）。
协议兼容百炼 Qwen-Audio-TTS / CosyVoice WebSocket 协议，透传全部能力：流式合成、SSML、指令控制、hot\_fix、声音复刻音色、任务取消。

与 HTTP 版 `POST /api/v1/audio/speech` 的区别：

| 维度 | HTTP 版 | WebSocket 版 |
|------|---------|--------------|
| 输入方式 | 一次性提交完整文本 | 流式分段输入（continue-task） |
| 输出方式 | 完整音频一次返回 | 音频帧实时流式下发 |
| 连接复用 | 每请求一连接 | 同连接可串行多任务 |
| 适用场景 | 离线合成、短文本 | 对话播报、长文本、低延迟流式场景 |

## 认证

三种方式任选其一：

| 方式 | 说明 |
|------|------|
| `Authorization: Bearer sk-xxx` 请求头 | 推荐（服务端 SDK） |
| `x-api-key: sk-xxx` 请求头 | 备选 |
| `?api_key=sk-xxx` 查询参数 | 浏览器 WebSocket 无法设置自定义请求头时使用 |

认证失败时网关会下发错误 JSON 帧并以 `1008` 状态关闭连接：

```json
{"error": {"code": "401", "type": "unauthorized", "message": "无效的 API 密钥"}}
```

## 支持模型

| 模型 | 计费 |
|------|------|
| `qwen/qwen-audio-3.0-tts-flash` | 按合成字符数，1 元/万字符 |
| `cosyvoice-*`、`sambert-*` 系列 | 以模型列表（GET /api/v1/models）价格为准 |

> 计费以上游返回的 `usage.characters`（实际合成字符数）为准；连接异常中断时对已发送文本兑底计费。

## 客户端事件

**run-task**：创建合成任务（model 支持带 `qwen/` 前缀，网关自动去除）

```json
{
  "header": {"action": "run-task", "task_id": "task-001", "streaming": "duplex"},
  "payload": {
    "task_group": "audio", "task": "tts", "function": "SpeechSynthesizer",
    "model": "qwen/qwen-audio-3.0-tts-flash",
    "parameters": {
      "text_type": "PlainText",
      "voice": "longanhuan_v3.6",
      "format": "mp3"
    },
    "input": {}
  }
}
```

parameters 常用字段：

| 字段 | 说明 |
|------|------|
| voice | 音色：预置音色名，或声音复刻生成的 voice\_id |
| text\_type | `PlainText`（默认）/ `SSML` |
| format | 音频格式：`mp3`（默认）/ `wav` / `pcm` 等 |
| sample\_rate | 采样率，如 24000 |
| volume / rate / pitch | 音量 / 语速 / 语调 |
| instruct | 指令控制（仅支持的模型），如语气、情绪描述 |

**continue-task**：流式追加待合成文本

```json
{
  "header": {"action": "continue-task", "task_id": "task-001", "streaming": "duplex"},
  "payload": {"input": {"text": "要合成的文本片段"}}
}
```

限制：单条 ≤ 20000 字符；单任务累计 ≤ 200000 字符；相邻两次发送间隔 ≤ 23 秒（超时连接会被上游关闭）。

**finish-task**：结束任务（正常合成全部剩余文本）

```json
{
  "header": {"action": "finish-task", "task_id": "task-001", "streaming": "duplex"},
  "payload": {"input": {}}
}
```

取消任务（丢弃未合成部分，已合成部分正常计费）：

```json
{"payload": {"input": {"directive": "cancel"}}}
```

## 服务端事件

| 事件 | 说明 |
|------|------|
| `task-started` | 任务创建成功 |
| `result-generated` | 合成进度：`sentence-begin` / `synthesis` / `sentence-end`（sentence-end 携带累计 `usage.characters`） |
| `task-finished` | 任务完成，`payload.usage.characters` 为计费字符数 |
| `task-failed` | 任务失败，`header.error_code` / `header.error_message` 携带原因 |

音频数据以**二进制帧**下发（与文本事件帧交替到达），拼接即为完整音频。

## 连接复用

收到 `task-finished`（或 `task-failed`）后，可在**同一连接**上直接再发 `run-task` 开启下一个任务，无需重新握手，每个任务独立计费。

## JavaScript 示例（浏览器）

```javascript
const ws = new WebSocket(
  "wss://limapi.com/api/v1/realtime/audio/speech?api_key=sk-xxx"
);
const audioChunks = [];

ws.onopen = () => {
  ws.send(JSON.stringify({
    header: { action: "run-task", task_id: "task-1", streaming: "duplex" },
    payload: {
      task_group: "audio", task: "tts", function: "SpeechSynthesizer",
      model: "qwen/qwen-audio-3.0-tts-flash",
      parameters: { voice: "longanhuan_v3.6", format: "mp3" },
      input: {}
    }
  }));
};

ws.onmessage = (e) => {
  if (typeof e.data === "string") {
    const msg = JSON.parse(e.data);
    if (msg.header.event === "task-started") {
      ws.send(JSON.stringify({
        header: { action: "continue-task", task_id: "task-1", streaming: "duplex" },
        payload: { input: { text: "你好，欢迎使用实时语音合成" } }
      }));
      ws.send(JSON.stringify({
        header: { action: "finish-task", task_id: "task-1", streaming: "duplex" },
        payload: { input: {} }
      }));
    } else if (msg.header.event === "task-finished") {
      console.log("合成完成，计费字符：", msg.payload.usage.characters);
      ws.close();
    }
  } else {
    audioChunks.push(e.data); // 二进制音频帧
  }
};
```

## Python 示例

```python
import json
import websocket  # pip install websocket-client

ws = websocket.create_connection(
    "wss://limapi.com/api/v1/realtime/audio/speech",
    header=["Authorization: Bearer sk-xxx"],
)
ws.send(json.dumps({
    "header": {"action": "run-task", "task_id": "task-1", "streaming": "duplex"},
    "payload": {
        "task_group": "audio", "task": "tts", "function": "SpeechSynthesizer",
        "model": "qwen/qwen-audio-3.0-tts-flash",
        "parameters": {"voice": "longanhuan_v3.6", "format": "mp3"},
        "input": {}
    }
}))

audio = bytearray()
while True:
    opcode, data = ws.recv_data()
    if opcode == websocket.ABNF.OPCODE_BINARY:
        audio += data
        continue
    msg = json.loads(data)
    event = msg["header"]["event"]
    if event == "task-started":
        ws.send(json.dumps({
            "header": {"action": "continue-task", "task_id": "task-1", "streaming": "duplex"},
            "payload": {"input": {"text": "你好，欢迎使用实时语音合成"}}
        }))
        ws.send(json.dumps({
            "header": {"action": "finish-task", "task_id": "task-1", "streaming": "duplex"},
            "payload": {"input": {}}
        }))
    elif event == "task-finished":
        break

ws.close()
open("speech.mp3", "wb").write(audio)
```

## 错误处理

| 场景 | 行为 |
|------|------|
| 无/无效 API Key | 错误帧 `401 unauthorized` + 1008 关闭 |
| 额度/余额不足 | 错误帧 `402` + 1008 关闭 |
| 模型被禁用/不在 Key 白名单/非 TTS 模型 | 合成 `task-failed`（error\_code=`ModelNotSupported`），连接保持 |
| 上游连接失败 | 错误帧 `502 upstream_unavailable` + 1008 关闭 |
| 合成过程中连接断开 | 已发送文本按字符数兑底计费 |
