Files
ZNJJ-api-server/docs/voice-websocket.md
2026-06-01 11:18:41 +08:00

11 KiB
Raw Blame History

Voice WebSocket 使用说明

基于 src/voice 产品语音管线与 static/voice-demo 浏览器示例整理。

概览

项目 说明
WebSocket 路径 /ws-product
协议标识 va.ws.v1JSON + base64音频上行也支持二进制 PCM
默认音频 PCM16 小端(pcm_s16le、16 kHz、单声道
会话 ID 连接 URL 查询参数 chatIdchat_id;未传时服务端自动生成
健康检查 GET /voice/health
浏览器 Demo 默认挂载于 /voice-demo(由 voice 配置 server.serve_webpage 控制)

完整 URL 示例:

ws://127.0.0.1:8000/ws-product?chatId=voice_abc123
wss://your-host/ws-product?chatId=voice_abc123

连接流程

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: WebSocket connect (?chatId=...)
    Server-->>Client: 101 Switching Protocols
    Client->>Server: session.start (JSON)
    Note over Client,Server: 可选:固定开场白 / FastGPT opener / LLM 生成问候
    loop 会话中
        Client->>Server: input.audio (binary 或 JSON)
        Client->>Server: input.text / input.image / response.cancel
        Server-->>Client: input.transcript.* / response.text.* / response.audio.*
        Server-->>Client: response.state若启用状态标签
    end
    Client->>Server: session.stop
    Server-->>Client: WebSocket close

推荐顺序(与 voice-demo/app.js 一致):

  1. 建立 WebSocket 连接(建议 binaryType = "arraybuffer")。
  2. 连接成功后立即发送 session.start
  3. 开始推送麦克风音频(二进制帧或 input.audio JSON
  4. 处理服务端 JSON 事件文本、转写、TTS 音频等)。
  5. 断开前发送 session.stop,再关闭连接。

消息信封

除二进制音频外,所有消息均为 UTF-8 JSON 对象。服务端下发事件统一包含:

字段 类型 说明
type string 事件类型
protocol string 固定为 va.ws.v1
seq number 单调递增序号(仅服务端事件)

客户端 → 服务端

session.start

开始会话,必须在发送音频或文本输入之前调用。

{
  "type": "session.start",
  "protocol": "va.ws.v1",
  "chatId": "voice_abc123",
  "audio": {
    "encoding": "pcm_s16le",
    "sample_rate": 16000,
    "channels": 1
  }
}

chatId 也可写作 chat_id。若省略,服务端使用 URL 查询参数或自动生成 ID。

session.stop

正常结束会话。

{
  "type": "session.stop",
  "reason": "client_disconnect"
}

input.audioJSON 形式)

{
  "type": "input.audio",
  "audio": "<base64 PCM16>",
  "sample_rate": 16000,
  "channels": 1
}

audio 字段也可命名为 datasample_rate / channels 可省略,默认与服务端配置一致。

二进制音频(推荐)

直接发送 原始 PCM16 小端 字节流,无需 JSON 包装。voice-demo 通过 AudioWorklet 每 20 ms 发送一帧16 kHz 单声道下约 640 字节/帧)。

服务端同时接受 JSON 与二进制两种上行格式。

input.text

发送文本回合;默认会打断当前 bot 回复(interrupt: true)。

{
  "type": "input.text",
  "text": "你好,我想报案",
  "interrupt": true
}

注意:文本输入不会input.transcript.final 回显,客户端需自行在 UI 中展示用户消息Demo 即如此处理)。

input.image

上传相机/图片并附带可选说明文本,供多模态 LLM 使用。

{
  "type": "input.image",
  "image": "<base64>",
  "mime_type": "image/jpeg",
  "width": 1920,
  "height": 1080,
  "text": "Answer using this camera image.",
  "append_to_context": true
}

约束:

  • 支持 image/jpegimage/pngimage/webp
  • 单张最大 8 MB
  • widthheight 必须为正整数
  • 支持 data:image/jpeg;base64,... 格式(会自动剥离前缀)

response.cancel

取消当前 bot 回复(等价于打断 TTS / LLM 流)。

{
  "type": "response.cancel"
}

服务端 → 客户端

用户语音转写

事件 说明
input.transcript.interim ASR 中间结果(流式识别过程中)
input.transcript.final 用户一句话结束后的最终转写
{
  "type": "input.transcript.final",
  "protocol": "va.ws.v1",
  "seq": 12,
  "text": "发生了交通事故",
  "user_id": "product-user",
  "timestamp": "2026-06-01T10:00:00.000Z"
}

助手文本流

文本通常早于对应 TTS 音频到达,便于客户端先渲染字幕。

事件 说明
response.text.started 新一轮助手回复开始
response.text.delta 流式文本片段
response.text.final 本轮文本结束;interrupted: true 表示被打断
{
  "type": "response.text.delta",
  "protocol": "va.ws.v1",
  "seq": 20,
  "text": "您好,"
}
{
  "type": "response.text.final",
  "protocol": "va.ws.v1",
  "seq": 45,
  "text": "您好,请问发生了什么情况?",
  "interrupted": false
}

助手语音TTS

事件 说明
response.audio.started Bot 开始说话
response.audio.delta PCM16 音频块base64
response.audio.stopped Bot 说完
{
  "type": "response.audio.delta",
  "protocol": "va.ws.v1",
  "seq": 30,
  "audio": "<base64 PCM16>",
  "bytes": 640,
  "sample_rate": 16000,
  "channels": 1
}

客户端应将各 delta 块按序解码并无缝拼接播放Demo 使用 Web Audio AudioContext 调度)。

助手状态(可选)

当 voice 配置启用 agent.response_stateLLM 输出开头的 <state>...</state> 标签会被剥离,并单独下发:

{
  "type": "response.state",
  "protocol": "va.ws.v1",
  "seq": 18,
  "state": "2000"
}

Demo 根据状态码展示拍照引导(如 20002015 等车险场景状态)。

音频参数

参数 默认值 说明
采样率 16000 Hz 配置项 audio.sample_rate_hz
声道 1mono 配置项 audio.channels
帧长 20 ms 配置项 audio.frame_ms;每帧 640 字节
编码 PCM signed 16-bit LE 小端有符号 16 位整数

会话与打断行为

  • chatId:同一 ID 用于 LLM如 FastGPT多轮上下文连接时可写在 URL 或 session.start 中。
  • 语音回合VAD + 静音超时判定用户说完;说完后触发 STT 最终转写与 LLM。
  • 打断:用户说话、input.textinterrupt: true)或 response.cancel 可打断 bot被打断的助手文本在 response.text.final 中带 interrupted: true
  • 空闲超时:长时间无活动会断开(session.inactivity_timeout_sec,默认 60 秒);可配置空闲提示语。
  • 开场白:由 agent.greeting_mode 控制(fixed / fastgpt_opener / generated 等)。

浏览器 Demo 参考实现

Demo 位于 static/voice-demo/,无构建步骤,核心文件:

文件 职责
app.js WebSocket 连接、事件处理、聊天 UI、TTS 播放
pcm-recorder.worklet.js 麦克风采集、重采样至 16 kHz、20 ms 二进制帧
index.html / styles.css 页面与样式

启动 Demo

  1. 启动 API 服务并加载 voice 配置(环境变量 VOICE_CONFIG 指向 JSON默认 config/voice.json)。
  2. 浏览器打开 http://127.0.0.1:8000/voice-demo/(挂载路径见配置 server.webpage_mount)。
  3. 点击 ConnectEnable mic 开始对话。

Demo 关键实现要点

连接与握手

const ws = new WebSocket("ws://127.0.0.1:8000/ws-product?chatId=voice_xxx");
ws.binaryType = "arraybuffer";

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: "session.start",
    protocol: "va.ws.v1",
    chatId: "voice_xxx",
    audio: { encoding: "pcm_s16le", sample_rate: 16000, channels: 1 },
  }));
};

发送麦克风(二进制,与 Demo 一致)

// AudioWorklet 每 20ms postMessage { type: "frame", buffer: ArrayBuffer }
recorderNode.port.onmessage = (event) => {
  if (event.data?.type === "frame") {
    ws.send(event.data.buffer);
  }
};

播放 TTS

function decodeBase64ToInt16(b64) {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
  return new Int16Array(bytes.buffer);
}

// 收到 response.audio.delta 后,将 Int16 转为 Float32 并调度到 AudioContext

发送文本并打断

ws.send(JSON.stringify({
  type: "input.text",
  text: "【拍摄完成】",
  interrupt: true,
}));
// 客户端应停止本地 TTS 播放队列;服务端会发 response.text.final(interrupted=true)

跨域静态页

若 Demo 托管在其他端口,需在 voice 配置中设置 server.cors_origins,并将 WebSocket URL 指向 API 主机。

浏览器 getUserMedia 需要安全上下文:https://http://localhost 可用;其他 HTTP 源需改用 HTTPS + wss://

最小客户端示例(伪代码)

const ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws-product?chatId=voice_demo_1`);
ws.binaryType = "arraybuffer";

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: "session.start",
    protocol: "va.ws.v1",
    audio: { encoding: "pcm_s16le", sample_rate: 16000, channels: 1 },
  }));
};

ws.onmessage = (event) => {
  if (typeof event.data !== "string") return;
  const msg = JSON.parse(event.data);
  switch (msg.type) {
    case "input.transcript.final":
      console.log("User:", msg.text);
      break;
    case "response.text.delta":
      process.stdout?.write?.(msg.text); // 流式打印助手文本
      break;
    case "response.audio.delta":
      playPcm16(decodeBase64(msg.audio));
      break;
    case "response.state":
      console.log("State:", msg.state);
      break;
  }
};

function disconnect() {
  ws.send(JSON.stringify({ type: "session.stop", reason: "done" }));
  ws.close(1000, "done");
}

健康检查响应示例

curl http://127.0.0.1:8000/voice/health
{
  "status": "healthy",
  "config": "/path/to/config/voice.json",
  "protocols": {
    "/ws-product": "va.ws.v1.json_base64"
  },
  "features": {
    "product_text_input": true,
    "product_text_interrupt": true
  },
  "demo": "/voice-demo",
  "llm_provider": "fastgpt",
  "stt_provider": "xfyun",
  "tts_provider": "xfyun"
}

常见问题

现象 可能原因
连接后立即断开 未发送 session.start;或超过 inactivity 超时
无 bot 语音 未处理 response.audio.deltaAudioContext 未在用户手势后 resume
回声/啸叫 建议使用耳机Demo 已开启浏览器 AEC但扬声器外放仍可能串音
文本发送无用户气泡 设计如此,需客户端本地展示 input.text 内容
跨域 WebSocket 失败 检查 cors_originswss 证书