Files
Cyrene/docs/api/backend-services/voice-service.md
T
AskaEth 6ef9e082a6 feat: 语音流式输入管线 + VAD前端集成 + 插件-工具合并清理
- 前端: VAD语音检测(@ricky0123/vad-web) + useVoiceInput双模式(流式WS/REST)
- Gateway: VoiceStreamManager代理WS流式STT到voice-service
- Voice-service: DashScope REST → Realtime WS → Whisper三级引擎 + ffmpeg转码
- 共享模块: pkg/audio(音频转换) + pkg/dashscope(ASR REST客户端)
- 清理: 移除旧plugin-manager和pkg/plugins,完成插件→工具合并
- 文档: 完善gateway-api.md和voice-service.md语音API文档
- 工具: scripts/voice/ 语音转换脚本集

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 11:50:40 +08:00

7.9 KiB

Voice-Service API

Base URL: http://<host>:8093 | Auth:

语音服务封装两层引擎:

  • STT (语音转文字): DashScope REST 离线模型 qwen3-asr-flash-2026-02-10 (主) → DashScope Realtime WS qwen3-asr-flash-realtime (流式) → 本地 Whisper (备)
  • TTS (文字转语音): edge-tts (主) + espeak-ng (备)

引擎分层说明:

  • 离线转录 (POST /api/v1/transcribe): 使用 DashScope REST API,无需 session 协商和 Server VAD,延迟更低。失败自动回退 Whisper。
  • 流式转录 (GET /api/v1/stt/stream): 使用 DashScope Realtime WebSocket,支持实时分片输入和中间结果输出,需客户端发送 PCM 音频。
  • 音频转码: 所有非 PCM 格式通过 ffmpeg 转码为 16kHz mono PCM 后再识别,支持 WAV/MP3/OGG/FLAC/WebM/Opus/AMR 等格式。

目录

  1. POST /api/v1/transcribe — 语音转文字
  2. POST /api/v1/tts/synthesize — 文字转语音
  3. GET /api/v1/tts/voices — 发音人列表
  4. GET /api/v1/health — 健康检查
  5. GET /api/v1/status — 服务状态
  6. GET /api/v1/tts/status — TTS 状态
  7. WebSocket GET /api/v1/stt/stream — 流式 STT

1. POST /api/v1/transcribe — 语音转文字

Content-Type: multipart/form-data | Max body: 10 MB

引擎选择

优先使用 DashScope REST 离线模型 qwen3-asr-flash-2026-02-10,失败自动回退本地 Whisper。

接收音频 → DashScope REST API (HTTP POST)
              ↓ 失败
           ffmpeg 转码 PCM → 本地 Whisper 引擎

表单字段

字段 类型 必填 说明
audio file 音频文件。支持格式: wav, mp3, ogg, flac, m4a, aac, webm, opus, amr, pcm
language string 默认 "zh"。可选: zh, en, ja, ko, auto

转码说明: 非 PCM 格式(含 Opus/WebM/AMR)通过 ffmpeg 自动转码为 16-bit PCM 16000Hz mono 后识别。需部署环境安装 ffmpeg。

响应 200

{
  "success": true,
  "text": "转录结果文本",
  "language": "zh",
  "duration_ms": 1234
}

错误

状态码 错误体
400 {"error":"文件过大或解析失败,最大支持 10MB"}
400 {"error":"缺少 audio 文件字段"}
400 {"error":"音频文件为空"}
400 {"error":"不支持的语言: <lang>,支持的语言: zh, en, ja, ko, auto"}
405 {"error":"method not allowed"}
500 {"error":"读取音频文件失败"}
500 {"success":false,"error":"<engine error>"}

2. POST /api/v1/tts/synthesize — 文字转语音

Content-Type: application/json

请求

{
  "text": "你好世界 (必填)",
  "voice": "zh-CN-XiaoxiaoNeural (默认)",
  "rate": "+0% (默认,如 +20%/-20%)"
}

响应 200 — 原始音频流

  • Content-Type: audio/mpeg (edge-tts) 或 audio/wav (espeak-ng/fallback)
  • Content-Disposition: inline; filename=synthesized.mp3

引擎回退链: edge-tts (mp3) → espeak-ng (wav) → silent WAV

错误

状态码 错误体
400 {"error":"请求体解析失败: ..."}
400 {"error":"text 字段不能为空"}
405 {"error":"method not allowed"}
500 {"error":"TTS 合成失败: ..."}

3. GET /api/v1/tts/voices — 发音人列表

// 响应 200
{
  "voices": [
    { "name": "zh-CN-XiaoxiaoNeural", "display_name": "晓晓 (女声)", "gender": "Female", "locale": "zh-CN" },
    { "name": "zh-CN-YunxiNeural",    "display_name": "云希 (男声)", "gender": "Male",   "locale": "zh-CN" },
    { "name": "zh-CN-XiaoyiNeural",   "display_name": "晓伊 (女声)", "gender": "Female", "locale": "zh-CN" }
  ],
  "count": 3
}

4. GET /api/v1/health — 健康检查

{
  "status": "ok",
  "service": "voice-service",
  "stt": {
    "available": true,
    "primary": "dashscope_rest",
    "dashscope_rest": {
      "available": true,
      "model": "qwen3-asr-flash-2026-02-10",
      "protocol": "rest"
    },
    "dashscope_ws": {
      "available": true,
      "model": "qwen3-asr-flash-realtime",
      "protocol": "websocket",
      "state": "idle"
    },
    "whisper": {
      "available": true,
      "binary_available": true,
      "model_loaded": true,
      "ffmpeg_available": true,
      "model_name": "ggml-small.bin"
    },
    "default_language": "zh",
    "supported_languages": ["zh","en","ja","ko","auto"]
  },
  "tts": {
    "available": true,
    "edge_tts": true,
    "espeak_ng": false,
    "engine": "edge-tts",
    "default_voice": "zh-CN-XiaoxiaoNeural",
    "builtin_voices": 3
  }
}

状态字段说明

字段 说明
stt.available DashScope REST / WS 或 Whisper 至少一个可用
stt.primary 当前优先引擎: dashscope_rest
stt.dashscope_rest.available DashScope REST API Key 已配置
stt.dashscope_rest.protocol 协议类型: rest
stt.dashscope_ws.available DashScope Realtime WS 可用
stt.dashscope_ws.protocol 协议类型: websocket
stt.dashscope_ws.state 连接状态: idle, connected, error
stt.whisper.available Whisper 二进制 + 模型文件 + ffmpeg 均存在
stt.whisper.ffmpeg_available ffmpeg 可用于音频转码
tts.available 至少一个 TTS 引擎可用
tts.engine 当前激活引擎: edge-tts, espeak-ng, fallback (silent WAV), none

5. GET /api/v1/status — 服务状态

/health 但无顶层 status 字段:

{
  "service": "voice-service",
  "stt": { ... },  // 同 health.stt
  "tts": { ... }   // 同 health.tts
}

6. GET /api/v1/tts/status — TTS 单独状态

{
  "service": "voice-service",
  "tts": {
    "available": true,
    "edge_tts": true,
    "espeak_ng": false,
    "engine": "edge-tts",
    "default_voice": "zh-CN-XiaoxiaoNeural",
    "builtin_voices": 3
  }
}

7. WebSocket GET /api/v1/stt/stream — 流式 STT

Query 参数: ?language=zh&format=pcm (language 默认 zh, format 默认 pcm) Read deadline: 300s

注意: 此端点使用 DashScope Realtime WebSocket (qwen3-asr-flash-realtime),音频帧必须是 PCM 格式。非 PCM 格式应使用 REST 离线转录 (POST /api/v1/transcribe)。

Gateway 代理: Gateway 的 voice_stream_* 消息类型通过此端点与前端 VAD 配合,实现端到端流式语音 → STT → LLM 管道。详见 Gateway WebSocket 文档

客户端 → 服务端

Binary 帧: 原始 PCM 音频 (16-bit LE, 16000Hz, mono)。每帧通过 input_audio_buffer.append 转发到 DashScope。

JSON 控制帧:

{ "action": "stop" }
// 请求结束会话。服务端返回 done 后关闭。

{ "language": "en" }
// 动态切换识别语言。

服务端 → 客户端 (JSON 文本帧)

result — 识别结果

{
  "type": "result",
  "text": "识别文本片段",
  "isFinal": true
}
字段 说明
isFinal: true VAD 端点检测到的完整句子
isFinal: false 中间增量 (delta)

error

{ "type": "error", "error": "错误描述" }

done — 响应 stop

{ "type": "done", "action": "stop" }

连接生命周期

  1. HTTP 升级请求 → 验证 STT 引擎可用性 (不可用返回 503)
  2. 建立 DashScope realtime 会话 (session.createdsession.updatesession.updated)
  3. 客户端发送 binary PCM 帧 → 服务端 base64 编码后 input_audio_buffer.append
  4. DashScope VAD 自动检测 → conversation.item.input_audio_transcription.completed → 转发 result
  5. 客户端发送 {"action":"stop"} → 服务端 session.finish → 关闭连接