# 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 — 语音转文字](#1-post-apiv1transcribe)
2. [POST /api/v1/tts/synthesize — 文字转语音](#2-post-apiv1ttssynthesize)
3. [GET /api/v1/tts/voices — 发音人列表](#3-get-apiv1ttsvoices)
4. [GET /api/v1/health — 健康检查](#4-get-apiv1health)
5. [GET /api/v1/status — 服务状态](#5-get-apiv1status)
6. [GET /api/v1/tts/status — TTS 状态](#6-get-apiv1ttsstatus)
7. [WebSocket GET /api/v1/stt/stream — 流式 STT](#7-websocket-get-apiv1sttstream)

---

## 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

```json
{
  "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`

### 请求

```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 — 发音人列表

```json
// 响应 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 — 健康检查

```json
{
  "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` 字段:

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

---

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

```json
{
  "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 文档](../../gateway-api.md#流式语音输入流程-voice_stream_)。

### 客户端 → 服务端

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

**JSON 控制帧:**

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

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

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

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

**error**
```json
{ "type": "error", "error": "错误描述" }
```

**done** — 响应 stop
```json
{ "type": "done", "action": "stop" }
```

### 连接生命周期

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