🚀 Node.js 接入腾讯云 WebSocket 实时语音合成(TTS)完整实践

10 阅读3分钟

在做 AI 对话 / 语音播报 / 实时助手 时,低延迟语音合成是一个绕不开的能力。
腾讯云提供了 WebSocket 实时语音合成(TTS) 接口,支持边合成边返回音频流,非常适合实时场景。

本文将基于 腾讯云官方文档,手把手实现一套 Node.js 可直接运行的 WebSocket 实时语音合成示例


一、效果预览

  • 使用 WebSocket 建立连接
  • 文本发送后,服务端 实时返回音频二进制流
  • 最终生成一个 pcm 音频文件
  • 延迟极低,适合实时播放

二、准备工作

1️⃣ 开通服务

在腾讯云控制台开通:

  • 语音合成(TTS)
  • 确保支持 实时语音合成(WebSocket)

2️⃣ 获取密钥

需要以下信息:

  • SecretId
  • SecretKey
  • AppId(数字)

建议通过 环境变量 注入:

export TENCENT_SECRET_ID=你的SecretId
export TENCENT_SECRET_KEY=你的SecretKey
export TENCENT_APPID=你的AppId

三、技术原理简述

腾讯云实时 TTS 的核心流程:

  1. 按规则拼接 URL 参数

  2. 使用 HMAC-SHA1 对请求字符串进行签名

  3. 建立 WebSocket 连接

  4. 服务端返回:

    • 二进制帧:音频数据
    • 文本帧:JSON 状态信息
  5. final = 1 表示合成结束


四、安装依赖

npm install ws uuid

Node.js 自带 crypto,无需额外安装。


五、完整 Node.js 示例代码

该代码可以 直接运行,并生成 tts_output.pcm 文件

const WebSocket = require("ws");
const crypto = require("crypto");
const { v4: uuidv4 } = require("uuid");
const fs = require("fs");

// 从环境变量读取密钥和 AppID
const SECRET_ID = process.env.TENCENT_SECRET_ID;
const SECRET_KEY = process.env.TENCENT_SECRET_KEY;
const APP_ID = process.env.TENCENT_APPID;

// 当前时间戳和过期时间
const timestamp = Math.floor(Date.now() / 1000);
const expired = timestamp + 300; // 签名 5 分钟有效

// 生成唯一会话 ID
const sessionId = uuidv4();

// 请求参数(必须严格按文档)
const params = {
  Action: "TextToStreamAudioWS",
  AppId: APP_ID,
  SecretId: SECRET_ID,
  Timestamp: timestamp,
  Expired: expired,
  SessionId: sessionId,
  Text: "你好,实时语音合成测试。",
  VoiceType: 101001,
  SampleRate: 16000,
  Codec: "pcm",
  Speed: 0,
  Volume: 0,
};

// 参数按字典序排序
const sortedKeys = Object.keys(params).sort();
const queryStr = sortedKeys
  .map((k) => `${k}=${encodeURIComponent(params[k])}`)
  .join("&");

// 构建签名源字符串
const method = "GET";
const host = "tts.cloud.tencent.com";
const path = "/stream_ws";
const originStr = `${method}${host}${path}?${queryStr}`;

// HMAC-SHA1 签名
const signature = crypto
  .createHmac("sha1", SECRET_KEY)
  .update(originStr)
  .digest("base64");

// WebSocket URL
const url = `wss://${host}${path}?${queryStr}&Signature=${encodeURIComponent(
  signature
)}`;

console.log("WebSocket URL:", url);

// 保存音频数据
const audioBuffers = [];
const ws = new WebSocket(url);

ws.on("open", () => {
  console.log("WebSocket 已连接");
});

ws.on("message", (data) => {
  // 二进制帧:音频数据
  if (data instanceof Buffer) {
    audioBuffers.push(data);
    console.log("收到音频分片:", data.length);
    return;
  }

  // 文本帧:状态信息
  try {
    const msg = JSON.parse(data.toString());
    console.log("文本消息:", msg);

    if (msg.final === 1) {
      console.log("语音合成完成");
      ws.close();
    }
  } catch (err) {
    console.error("JSON 解析失败:", err);
  }
});

ws.on("close", () => {
  const audio = Buffer.concat(audioBuffers);
  fs.writeFileSync("tts_output.pcm", audio);
  console.log("音频已保存为 tts_output.pcm");
});

ws.on("error", (err) => {
  console.error("WebSocket 错误:", err);
});

六、关键参数说明

VoiceType(音色)

音色
女声标准101001
男声标准101002
情感音色需看套餐

Codec(音频格式)

格式说明
pcm延迟最低,适合实时播放
mp3压缩格式,体积小

实时场景强烈推荐 pcm


七、常见坑总结

1️⃣ 参数顺序错误

  • 签名必须基于“排序后的参数”
  • 少一个参数都会鉴权失败

2️⃣ timestamp / expired 不合法

  • Expired 必须大于 Timestamp
  • 过期会直接 403

3️⃣ 音频不是完整 wav

  • WebSocket 返回的是 纯音频流
  • pcm 需要自行加 wav 头或直接播放

4️⃣ 文本不要太长

  • 建议 ≤ 500 字
  • 长文本应拆句,多次请求

八、如何真正做到“实时播放”

正确做法是:

  • 服务端收到音频分片
  • 立即推送给前端
  • 前端使用 AudioContext 播放 PCM

❌ 错误做法:
等全部音频返回后再播放(那就不实时了)


九、适用场景

  • 🤖 AI 对话语音播报
  • 📢 实时通知系统
  • 🎧 语音助手
  • 🧠 大模型语音输出