跳转到主要内容
CosyVoice

CosyVoice WebSocket API

CosyVoice 语音合成 WebSocket 接口协议

CosyVoice 文本转语音的 WebSocket 接口参数与协议。DashScope SDK 仅支持 Java 和 Python,其他语言请使用 WebSocket 接口。 用户指南: 模型概览和音色选择,参见语音合成 WebSocket 支持全双工通信。客户端与服务器通过一次握手建立持久连接,然后实时相互推送数据。 常用的 WebSocket 库:
  • Go: gorilla/websocket
  • PHP: Ratchet
  • Node.js: ws
CosyVoice 模型仅支持 WebSocket,不支持 HTTP REST API。 使用 HTTP 请求(POST、GET)会返回 InvalidParameter 或 URL 错误。

前提条件

获取 API Key

模型与定价

参见语音合成

文本与格式限制

文本长度限制

每条续传指令最多发送 20,000 个字符。所有续传指令的累计字符数不得超过 200,000。

字符计数规则

  • 中文字符(简体、繁体、日文汉字、韩文汉字)按 2 个字符计算。其他字符(标点、字母、数字、假名/谚文)按 1 个字符计算。
  • SSML 标签不计入字符数。
  • 示例:
    • "你好" → 2 + 2 = 4 个字符
    • "中A文123" → 2 + 1 + 2 + 1 + 1 + 1 = 8 个字符
    • "中文。" → 2 + 2 + 1 = 5 个字符
    • "中 文。" → 2 + 1 + 2 + 1 = 6 个字符
    • "<speak>你好</speak>" → 2 + 2 = 4 个字符

编码格式

使用 UTF-8 编码。

数学表达式支持

数学表达式解析功能适用于 cosyvoice-v3-flash 和 cosyvoice-v3-plus,涵盖常见的中小学数学内容,包括基本运算、代数和几何。
该功能仅支持中文。
详见将 LaTeX 公式转为语音(仅中文)

SSML 支持

使用 SSML 需同时满足以下条件:
  1. 模型: 仅 cosyvoice-v3-flash 和 cosyvoice-v3-plus 支持 SSML。
  2. 音色: 使用支持 SSML 的音色:
    • 所有克隆音色(通过音色克隆 API 创建)。
    • 音色列表中标记为支持 SSML 的系统音色。
    不支持 SSML 的系统音色(如部分基础音色)即使开启了 enable_ssml,仍会返回错误 "SSML text is not supported at the moment!"。
  3. 参数: 在启动指令中将 enable_ssml 设置为 true
然后通过续传指令发送 SSML 格式的文本。完整示例参见快速开始

交互流程

交互流程
客户端到服务器的消息为指令,服务器到客户端的消息为 JSON 事件或二进制音频流。 交互序列:
  1. 建立 WebSocket 连接。
  2. 发送启动指令开始任务。
  3. 等待收到任务已启动事件后再继续。
  4. 发送文本: 按顺序发送一条或多条续传指令。服务器收到完整句子后,返回结果已生成事件和音频流。文本长度约束参见续传指令中的 text 字段。
    按顺序发送多条续传指令提交文本片段。服务器会自动将文本按句子分段:
    • 完整句子会立即合成。
    • 不完整的句子会缓存,直到收到完整句子。不完整句子不会返回音频。
    收到结束指令后,服务器会强制合成所有缓存内容。
  5. 通过 binary 通道接收音频流。
  6. 发送完所有文本后,发送结束指令。继续接收音频流。不要跳过此步骤,否则音频末尾可能丢失。
  7. 从服务器接收任务已完成事件
  8. 关闭 WebSocket 连接。
建议复用 WebSocket 连接来执行多个任务,而非每次都新建连接。参见连接开销与复用
保持 task_id 一致:单个任务中的启动指令、所有续传指令和结束指令必须使用同一个 task_idtask_id 不匹配会导致
  • 音频推送乱序。
  • 语音内容错位。
  • 任务状态异常,可能无法收到任务已完成事件。
  • 计费失败或使用量统计不准确。
最佳实践
  • 发送启动指令时生成唯一的 task_id(如 UUID)。
  • 将 task_id 存入变量。
  • 后续所有续传指令和结束指令使用该 task_id。
  • 收到任务已完成事件后,为下一个任务生成新的 task_id。

客户端实现要点

服务端与客户端职责

服务端职责 服务端按顺序推送完整的音频流,无需处理音频排序或完整性。 客户端职责
  1. 读取并拼接所有音频片段 服务端将音频拆分为多个二进制帧。接收所有帧并拼接:
# Python:拼接音频片段
with open("output.mp3", "ab") as f:  # 追加模式
  f.write(audio_chunk)  # audio_chunk 为每个接收到的二进制音频片段
// JavaScript:拼接音频片段
const audioChunks = [];
ws.onmessage = (event) => {
  if (event.data instanceof Blob) {
    audioChunks.push(event.data);  // 收集所有音频片段
  }
};
// 任务完成后合并音频
const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' });
  1. 保持完整的 WebSocket 生命周期 从发送启动指令到接收任务已完成事件期间,不要断开连接。常见错误:
    • 音频未接收完毕就关闭连接,导致音频不完整。
    • 忘记发送结束指令,导致文本缓存未处理。
    • 页面跳转或应用后台化时未处理 WebSocket 心跳保活。
    移动端(Flutter、iOS、Android)进入后台时需要特殊网络处理。在后台任务或服务中保持 WebSocket 连接,或返回前台时重新初始化。
  2. ASR → LLM → TTS 流程中的文本完整性 确保传入 TTS 的文本完整:
    • 等待 LLM 生成完整句子后再发送续传指令,而非逐字流式传输。
    • 流式合成时,按自然句边界(句号、问号)发送文本。
    • LLM 生成完成后,务必发送结束指令,避免末尾内容丢失。

平台特定提示

  • Flutter: 在 dispose 方法中关闭连接,防止使用 web_socket_channel 时内存泄漏。处理应用生命周期事件(如 AppLifecycleState.paused)以应对后台切换。
  • Web(浏览器): 部分浏览器限制 WebSocket 连接数量。多个任务复用同一个连接。使用 beforeunload 在页面关闭前关闭连接。
  • 移动端(iOS/Android 原生): 应用进入后台时,操作系统可能暂停或终止网络连接。使用后台任务或前台服务保持 WebSocket 活跃,或返回前台时重新初始化任务。

URL

wss://dashscope.aliyuncs.com/api-ws/v1/inference
常见 URL 错误
  • 协议错误: 使用 wss://,不要使用 http://https://
  • 认证参数放在 URL 中: 不要在 URL 中放置 Authorization(如 ?Authorization=bearer YOUR_API_KEY)。应在 HTTP 握手头中设置。参见请求头
  • 多余的路径片段: 不要在 URL 后拼接模型名或其他参数。模型在启动指令payload.model 中指定。

请求头

参数类型必填描述
Authorizationstring认证令牌。格式:bearer $DASHSCOPE_API_KEY
user-agentstring客户端标识,用于来源追踪。
X-DashScope-WorkSpacestring千问云业务空间 ID。
X-DashScope-DataInspectionstring数据合规检查。默认 enable。非必需不要设置。
认证时机认证发生在 WebSocket 握手阶段,而非发送启动指令时。如果 Authorization 头缺失或无效,服务器会以 HTTP 401 或 403 拒绝握手。客户端库通常将其报告为 WebSocketBadStatus 异常。

排查认证失败

如果 WebSocket 连接失败:
  1. 检查 API Key 格式: 确认 Authorization 头使用 bearer $DASHSCOPE_API_KEY 格式,bearer 与 Key 之间有空格。
  2. 验证 API Key 有效性: 查看 API Key 页面,确认 Key 处于激活状态且已授权 CosyVoice 模型。
  3. 检查头设置位置: 在 WebSocket 握手时设置 Authorization 头。各语言示例:
    • Python (websockets):extra_headers={"Authorization": f"bearer {api_key}"}
    • JavaScript:浏览器原生 WebSocket API 不支持自定义头。使用服务端代理或其他库(如 ws)。
    • Go (gorilla/websocket):header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
  4. 测试网络连通性: 使用 curl 或 Postman 调用其他支持 HTTP 的 DashScope API,验证 API Key 是否有效。

浏览器中使用 WebSocket

浏览器 new WebSocket(url) API 在握手时不支持自定义请求头(包括 Authorization)。无法直接从前端的代码认证。 解决方案:使用后端代理
  1. 从后端(Node.js、Java 或 Python)连接 CosyVoice,后端可以设置 Authorization 头。
  2. 前端通过 WebSocket 连接到你的后端,后端转发消息到 CosyVoice。
  3. 这样可以隐藏 API Key,同时可以添加认证、日志或限流等功能。
不要将 API Key 硬编码在前端代码中。Key 泄露可能导致账户被盗用、产生意外费用或数据泄露。
示例代码 其他语言可参考以下示例实现相同逻辑,或使用 AI 工具转换。

指令 (客户端到服务器)

指令是以 WebSocket 文本帧发送的 JSON 消息,用于控制任务生命周期。 按以下顺序发送指令:
  1. 发送启动指令
  2. 发送续传指令
  3. 发送结束指令
    • 结束任务。
    • 在所有续传指令发送完成后发送。

1. 启动指令 (run-task instruction): 启动任务

启动文本转语音任务。在此配置音色、采样率等参数。
  • 发送时机: WebSocket 连接建立后发送。
  • 不要在此发送文本。 文本通过续传指令发送。
  • input 字段必填,但值必须为 {}。省略会导致 InvalidParameter 错误("Missing required parameter 'payload.input'! Please follow the protocol!")。
示例
{
  "header": {
    "action": "run-task",
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "streaming": "duplex"
  },
  "payload": {
    "task_group": "audio",
    "task": "tts",
    "function": "SpeechSynthesizer",
    "model": "cosyvoice-v3-flash",
    "parameters": {
      "text_type": "PlainText",
      "voice": "longanyang",
      "format": "mp3",
      "sample_rate": 22050,
      "volume": 50,
      "rate": 1,
      "pitch": 1
    },
    "input": {}
  }
}
header 参数
参数类型必填描述
header.actionstring固定值:"run-task"。
header.task_idstring32 位 UUID。连字符可选(如 "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx""2bf83b9abaeb4fda8d9axxxxxxxxxxxx")。多数语言提供内置的 UUID 生成 API。
header.streamingstring固定值:"duplex"。
Python 生成 task_id 示例:
import uuid

def generateTaskId(self):
  # 生成随机 UUID
  return uuid.uuid4().hex
后续所有续传指令结束指令使用同一个 task_id。 payload 参数
参数类型必填描述
payload.task_groupstring固定值:"audio"。
payload.taskstring固定值:"tts"。
payload.functionstring固定值:"SpeechSynthesizer"。
payload.modelstring文本转语音模型。参见音色列表
payload.inputobject必填,但在启动指令中必须为空({})。 文本通过续传指令发送。
常见错误: 省略 input 字段或添加意外字段(如 mode、content)会导致 "InvalidParameter: task can not be null" 或连接关闭(WebSocket 码 1007)。
payload.parameters 参数
参数类型必填描述
text_typestring固定值:"PlainText"。
voicestring用于合成的音色。参见音色列表查看可用的系统音色。
formatstring音频格式。支持 pcm、wav、mp3(默认)和 opus。opus 格式可通过 bit_rate 调整码率。
sample_rateinteger采样率(Hz)。默认 22050。可选值:8000、16000、22050、24000、44100、48000。
volumeinteger音量。默认 50。范围 [0, 100],线性缩放。0 为静音,100 为最大音量。
ratefloat语速。默认 1.0。范围 [0.5, 2.0]。小于 1.0 减慢语速,大于 1.0 加快。
pitchfloat音调倍数。默认 1.0。范围 [0.5, 2.0]。与感知音调的关系并非严格线性,建议测试选择合适的值。
enable_ssmlboolean是否启用 SSML。设为 true 时仅允许发送一条续传指令
bit_rateint音频码率(Opus 格式),单位 kbps。默认 32。范围 [6, 510]。
word_timestamp_enabledboolean是否启用词级时间戳。默认 false。仅支持音色列表中标记为支持的系统音色。
seedint生成的随机种子。相同种子且参数一致时,输出结果可复现。默认 0。范围 [0, 65535]。
language_hintsarray[string]合成目标语言。可选值:zh、en、fr、de、ja、ko、ru、pt、th、id、vi。该字段为数组,但仅处理第一个元素。
instructionstring控制合成效果,如方言、情感或说话风格。仅支持音色列表中标记为支持 Instruct 的系统音色。最大长度 100 个字符。
enable_aigc_tagboolean是否在生成的音频中嵌入不可见的 AIGC 标识符。设为 true 时,标识符会嵌入 WAV、MP3 和 Opus 格式中。默认 false。cosyvoice-v3-flash 和 cosyvoice-v3-plus 支持。
aigc_propagatorstring设置 AIGC 标识符中的 ContentPropagator 字段。仅在 enable_aigc_tagtrue 时生效。默认 UID。cosyvoice-v3-flash 和 cosyvoice-v3-plus 支持。
aigc_propagate_idstring设置 AIGC 标识符中的 PropagateID 字段。仅在 enable_aigc_tagtrue 时生效。默认为当前请求 ID。cosyvoice-v3-flash 和 cosyvoice-v3-plus 支持。
hot_fixobject文本热补丁配置。合成前自定义发音或替换文本。仅 cosyvoice-v3-flash 支持。
enable_markdown_filterboolean是否启用 Markdown 过滤。合成前移除输入文本中的 Markdown 符号。默认 false。仅 cosyvoice-v3-flash 支持。
启用 word_timestamp_enabled 后,时间戳会出现在结果已生成事件中:
{
  "header": {
    "task_id": "3f39be22-efbd-4844-91d5-xxxxxxxxxxxx",
    "event": "result-generated",
    "attributes": {}
  },
  "payload": {
    "output": {
      "sentence": {
        "index": 0,
        "words": [
          {
            "text": "bed",
            "begin_index": 0,
            "end_index": 1,
            "begin_time": 280,
            "end_time": 640
          }
        ]
      }
    }
  }
}
cosyvoice-v3-flash 克隆音色的 instruction 示例:
请用粤语说话。(支持的方言:粤语、东北、甘肃、贵州、河南、湖北、江西、闽南、宁夏、山西、陕西、山东、上海话、四川、天津、云南。)
请尽可能大声地说一句话。
请尽可能慢地说一句话。
请尽可能快地说一句话。
请非常轻柔地说一句话。
你能说慢一点吗?
你能说得非常快吗?
你能说非常慢吗?
你能说快一点吗?
请非常生气地说一句话。
请非常开心地说一句话。
请非常害怕地说一句话。
请非常伤心地说一句话。
请非常惊讶地说一句话。
请尽量听起来坚定一些。
请尽量听起来生气一些。
请使用亲切的语气。
请用冰冷的语气说话。
请用威严的语气说话。
我想体验自然的语气。
我想看看你怎么表达威胁。
我想看看你怎么表达智慧。
我想看看你怎么表达诱惑。
我想听你生动地说话。
我想听你充满热情地说话。
我想听你沉稳地说话。
我想听你充满自信地说话。
你能兴奋地和我说话吗?
你能表现出傲慢的情绪吗?
你能表现出优雅的情绪吗?
你能愉快地回答问题吗?
你能给出温柔的情感表达吗?
你能用平静的语气和我说话吗?
你能深入地回答我吗?
你能用粗犷的态度和我说话吗?
用阴险的声音告诉我答案。
用坚定的声音告诉我答案。
用自然友好的聊天风格叙述。
用广播剧播客的语气说话。
cosyvoice-v3-flash 系统音色的 instruction 必须使用固定格式,参见音色列表 hot_fix 示例:
"hot_fix": {
  "pronunciation": [
  {"weather": "tian1 qi4"}
  ],
  "replace": [
  {"today": "jin1 tian1"}
  ]
}

2. 续传指令 (continue-task instruction)

发送待合成的文本。可以一次性发送全部文本,也可以按顺序拆分为多条指令发送。
发送时机: 收到任务已启动事件后。
文本片段之间的间隔不能超过 23 秒,否则会返回 "request timeout after 23 seconds" 错误。如果没有更多文本需要发送,请发送结束指令结束任务。23 秒超时由服务端强制执行,无法修改。
示例
{
  "header": {
    "action": "continue-task",
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "streaming": "duplex"
  },
  "payload": {
    "input": {
      "text": "床前明月光,疑是地上霜。"
    }
  }
}
header 参数
参数类型必填描述
header.actionstring固定值:"continue-task"。
header.task_idstring必须与启动指令中的 task_id 一致。
header.streamingstring固定值:"duplex"。
payload 参数
参数类型必填描述
input.textstring待合成的文本。

3. 结束指令 (finish-task instruction): 结束任务

结束任务。务必发送此指令,否则:
  • 音频不完整: 服务器不会强制合成缓存中的句子,导致音频末尾丢失。
  • 连接超时: 最后一条续传指令后等待超过 23 秒会触发超时。
  • 计费问题: 使用量信息可能不准确。
发送时机: 所有续传指令发送完成后立即发送。不要等待音频播放完成——这可能导致超时。
示例
{
  "header": {
    "action": "finish-task",
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "streaming": "duplex"
  },
  "payload": {
    "input": {}
  }
}
header 参数
参数类型必填描述
header.actionstring固定值:"finish-task"。
header.task_idstring必须与启动指令中的 task_id 一致。
header.streamingstring固定值:"duplex"。
payload 参数
参数类型必填描述
payload.inputobject固定值:{}

事件 (服务器到客户端)

事件是服务器发送的 JSON 消息,标记任务生命周期的各个阶段。
二进制音频单独发送,不包含在任何事件中。

1. 任务已启动事件 (task-started event)

确认任务已启动。仅在收到此事件后才能发送续传指令结束指令,否则任务会失败。 task-started 事件的 payload 为空。 示例
{
  "header": {
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "event": "task-started",
    "attributes": {}
  },
  "payload": {}
}
header 参数
参数类型描述
header.eventstring固定值:"task-started"。
header.task_idstring客户端生成的任务 ID。

2. 结果已生成事件 (result-generated event)

发送续传指令结束指令期间,服务器会持续返回 result-generated 事件和二进制音频帧。 每个 result-generated 事件包含当前句子索引。音频数据以二进制帧形式在事件之间到达。一个句子对应多个二进制音频帧。按顺序接收并追加到同一个文件中。 示例
{
  "header": {
    "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
    "event": "result-generated",
    "attributes": {}
  },
  "payload": {
    "output": {
      "sentence": {
        "index": 0,
        "words": []
      }
    },
    "usage": {
      "characters": 11
    }
  }
}
header 参数
参数类型描述
header.eventstring固定值:"result-generated"。
header.task_idstring客户端生成的任务 ID。
header.attributesobject附加属性——通常为空。
payload 参数
参数类型描述
payload.output.typestring句子事件类型。值为 sentence-begin(句子开始)、sentence-synthesis(句子合成中)或 sentence-end(句子结束)。
payload.output.sentence.indexinteger句子编号,从 0 开始。
payload.output.sentence.wordsarray词信息数组。
payload.output.sentence.words.textstring词文本。
payload.output.sentence.words.begin_indexinteger词在句子中的起始位置,从 0 开始计数。
payload.output.sentence.words.end_indexinteger词在句子中的结束位置,从 1 开始计数。
payload.output.sentence.words.begin_timeinteger词音频的起始时间戳,单位毫秒。
payload.output.sentence.words.end_timeinteger词音频的结束时间戳,单位毫秒。
payload.output.original_textstring当前句子的原始文本。仅在 typesentence-beginsentence-end 时出现。
payload.usage.charactersinteger截至目前的累计计费字符数。usage 字段仅在 typesentence-end 的事件中出现,请以最后一次出现的值为准。

3. 任务已完成事件 (task-finished event)

标记任务结束。 任务结束后,可以关闭 WebSocket 连接或复用它发送新的启动指令(参见连接开销与复用)。 示例
{
  "header": {
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "event": "task-finished",
    "attributes": {}
  },
  "payload": {
    "output": {}
  }
}
header 参数
参数类型描述
header.eventstring固定值:"task-finished"。
header.task_idstring客户端生成的任务 ID。

4. 任务失败事件 (task-failed event)

表示任务失败。关闭 WebSocket 连接并查看错误信息。 示例
{
  "header": {
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "event": "task-failed",
    "error_code": "InvalidParameter",
    "error_message": "[tts:]Engine return error code: 418",
    "attributes": {}
  },
  "payload": {}
}
header 参数
参数类型描述
header.eventstring固定值:"task-failed"。
header.task_idstring客户端生成的任务 ID。
header.error_codestring错误类型。
header.error_messagestring详细错误原因。

任务中断

流式合成过程中,可以提前中断当前任务(如用户取消播放),使用以下方式之一:
中断方式服务端行为使用场景
关闭连接立即停止合成。丢弃未发送的音频。不返回任务已完成事件。连接无法复用。立即停止: 用户取消播放、切换内容或退出应用。
发送结束指令强制合成缓存中的文本。返回剩余音频和任务已完成事件。连接可复用。优雅结束: 停止发送文本但接收所有缓存音频。

连接开销与复用

WebSocket 服务支持连接复用。 发送启动指令启动任务,发送结束指令结束任务。收到任务已完成事件后,复用同一个连接发送新的启动指令。
  1. 仅在收到任务已完成事件后发送新的启动指令
  2. 同一个连接上的不同任务使用不同的 task_id。
  3. 失败的任务会触发任务失败事件并关闭连接(无法复用)。
  4. 连接空闲 60 秒后超时断开。

性能与并发

并发限制

参见限速 如需提高并发配额,请联系客服。配额调整需要审核,通常需要 1-3 个工作日。
最佳实践: 复用 WebSocket 连接来执行多个任务。参见连接开销与复用

连接延迟

典型连接时间
  • 跨境连接:1-3 秒。极少数情况下 10-30 秒。
排查连接过慢(>30 秒)
  1. 网络延迟: 检查跨境连接质量或 ISP 性能。
  2. DNS 过慢: 尝试使用公共 DNS(8.8.8.8)或为 dashscope.aliyuncs.com 配置本地 hosts 文件。
  3. TLS 握手: 升级到 TLS 1.2 或更高版本。
  4. 代理/防火墙: 企业网络可能阻止或限制 WebSocket 连接。
排查工具
  • 使用 Wireshark 或 tcpdump 分析 TCP 握手、TLS 握手和 WebSocket Upgrade 的耗时。
  • 使用 curl 测试 HTTP 延迟:curl -w "@curl-format.txt" -o /dev/null -s https://dashscope.aliyuncs.com

音频生成速度

  • 实时率(RTF):0.1-0.5 倍实时(1 秒音频需要 0.1-0.5 秒生成)。实际速度因模型、文本长度和服务器负载而异。
  • 首包延迟:发送续传指令到收到首个音频片段需 200-800 毫秒。

示例代码

基础连通性示例。请根据实际业务场景实现生产级逻辑。建议使用异步编程同时发送和接收:
  1. 连接: 使用 WebSocket 库的 connect 函数,携带请求头URL建立连接。
  2. 监听消息: 服务器推送二进制音频和事件 事件
    • task-started: 任务已启动。仅在此之后发送续传指令结束指令
    • result-generated: 发送续传指令结束指令后持续返回。
    • task-finished: 任务完成。关闭连接。
    • task-failed: 任务失败。关闭连接并检查错误。
    二进制音频
    • MP3/Opus 流式播放:使用流式播放器(FFmpeg、PyAudio、AudioFormat、MediaSource)。不要逐帧播放。
    • 保存完整音频:以追加模式将帧写入同一个文件。
    • WAV/MP3:仅首帧包含头部信息,后续帧仅有音频数据。
  3. 发送指令: 从独立线程向服务器发送指令。
  4. 关闭连接: 完成、出错或收到任务已完成事件/任务失败事件后关闭。
  • Go
  • C#
  • PHP
  • Node.js
  • Java
  • Python
package main

import (
  "encoding/json"
  "fmt"
  "net/http"
  "os"
  "strings"
  "time"

  "github.com/google/uuid"
  "github.com/gorilla/websocket"
)

const (
  wsURL      = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/"
  outputFile = "output.mp3"
)

func main() {
  // 如果未设置环境变量,替换下一行为: apiKey := "YOUR_API_KEY"
  apiKey := os.Getenv("DASHSCOPE_API_KEY")

  // 清空输出文件
  os.Remove(outputFile)
  os.Create(outputFile)

  // 连接 WebSocket
  header := make(http.Header)
  header.Add("X-DashScope-DataInspection", "enable")
  header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))

  conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
  if err != nil {
    if resp != nil {
      fmt.Printf("Connection failed HTTP status code: %d\n", resp.StatusCode)
    }
    fmt.Println("Connection failed:", err)
    return
  }
  defer conn.Close()

  // 生成任务 ID
  taskID := uuid.New().String()
  fmt.Printf("Generated task ID: %s\n", taskID)

  // 发送启动指令
  runTaskCmd := map[string]interface{}{
    "header": map[string]interface{}{
      "action":    "run-task",
      "task_id":   taskID,
      "streaming": "duplex",
    },
    "payload": map[string]interface{}{
      "task_group": "audio",
      "task":       "tts",
      "function":   "SpeechSynthesizer",
      "model":      "cosyvoice-v3-flash",
      "parameters": map[string]interface{}{
        "text_type":   "PlainText",
        "voice":       "longanyang",
        "format":      "mp3",
        "sample_rate": 22050,
        "volume":      50,
        "rate":        1,
        "pitch":       1,
        // 如果 enable_ssml 为 true,仅允许发送一条续传指令。否则会返回"Text request limit violated, expected 1."
        "enable_ssml": false,
      },
      "input": map[string]interface{}{},
    },
  }

  runTaskJSON, _ := json.Marshal(runTaskCmd)
  fmt.Printf("Sent run-task instruction: %s\n", string(runTaskJSON))

  err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
  if err != nil {
    fmt.Println("Failed to send run-task:", err)
    return
  }

  textSent := false

  // 处理消息
  for {
    messageType, message, err := conn.ReadMessage()
    if err != nil {
      fmt.Println("Failed to read message:", err)
      break
    }

    // 处理二进制消息
    if messageType == websocket.BinaryMessage {
      fmt.Printf("Received binary message, length: %d\n", len(message))
      file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
      file.Write(message)
      file.Close()
      continue
    }

    // 处理文本消息
    messageStr := string(message)
    fmt.Printf("Received text message: %s\n", strings.ReplaceAll(messageStr, "\n", ""))

    // 简易 JSON解析获取事件类型
    var msgMap map[string]interface{}
    if json.Unmarshal(message, &msgMap) == nil {
      if header, ok := msgMap["header"].(map[string]interface{}); ok {
        if event, ok := header["event"].(string); ok {
          fmt.Printf("Event type: %s\n", event)

          switch event {
          case "task-started":
            fmt.Println("=== Received task-started event ===")

            if !textSent {
              // 发送续传指令

              texts := []string{"床前明月光,疑是地上霜。", "举头望明月,低头思故乡。"}

              for _, text := range texts {
                continueTaskCmd := map[string]interface{}{
                  "header": map[string]interface{}{
                    "action":    "continue-task",
                    "task_id":   taskID,
                    "streaming": "duplex",
                  },
                  "payload": map[string]interface{}{
                    "input": map[string]interface{}{
                      "text": text,
                    },
                  },
                }

                continueTaskJSON, _ := json.Marshal(continueTaskCmd)
                fmt.Printf("Sent continue-task instruction: %s\n", string(continueTaskJSON))

                err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
                if err != nil {
                  fmt.Println("Failed to send continue-task:", err)
                  return
                }
              }

              textSent = true

              // 发送结束指令前延迟
              time.Sleep(500 * time.Millisecond)

              // 发送结束指令
              finishTaskCmd := map[string]interface{}{
                "header": map[string]interface{}{
                  "action":    "finish-task",
                  "task_id":   taskID,
                  "streaming": "duplex",
                },
                "payload": map[string]interface{}{
                  "input": map[string]interface{}{},
                },
              }

              finishTaskJSON, _ := json.Marshal(finishTaskCmd)
              fmt.Printf("Sent finish-task instruction: %s\n", string(finishTaskJSON))

              err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
              if err != nil {
                fmt.Println("Failed to send finish-task:", err)
                return
              }
            }

          case "task-finished":
            fmt.Println("=== Task completed ===")
            return

          case "task-failed":
            fmt.Println("=== Task failed ===")
            if header["error_message"] != nil {
              fmt.Printf("Error message: %s\n", header["error_message"])
            }
            return

          case "result-generated":
            fmt.Println("Received result-generated event")
          }
        }
      }
    }
  }
}