# 讯飞 ASR 集成踩坑笔记

> demo 期间累积的所有讯飞接入坑点。建议入 TPS 知识库，作为未来接入新 ASR 供应商时的对照。

---

## 选型坑

### 坑 1：不要用"大模型版"

- 讯飞开放平台有两个录音转写产品：
  - 「录音文件转写（**标准版**）」← **用这个**
  - 「录音文件转写（**大模型版**）」← 不要用
- **大模型版的免费包阉割了 `language` 参数**，调用时会报 `language verify fail`
- 这个错误信息**严重误导**：看上去是语种没设对，实际是免费包没开通该参数权限
- 排查耗时：约 30 分钟

### 坑 2：消费端和开发端密钥不通

- 「讯飞听见 App」属于**消费端**，给最终用户做人工转写用
- 「讯飞开放平台」属于**开发端**，给开发者调 API 用
- **两套密钥互不相通**——开发端的 APP_ID 在消费端没用，反之亦然
- 如果只在讯飞听见 App 注册了账号，**没有** APP_ID 和 SecretKey，必须再去开放平台注册一次

### 坑 3：免费额度不会自动激活

- 开放平台首页显示"5 小时免费试用包"，但**只有走 0 元购买流程**才会激活
- 看到额度但没下单 → 调用时会报余额不足
- 流程：商品页 → 0 元下单 → 支付（实际免费）→ 额度到账

---

## 音频规格坑

### 坑 4：必须是 16kHz / 单声道 / 16bit WAV

- 其他规格讯飞会报错（且错误信息可能误导成"language verify fail"或"audio decode error"）
- MP3 在标准版**也支持**，但保险起见统一转 WAV
- 转码命令（ffmpeg）：
  ```bash
  ffmpeg -i input.mp3 -ac 1 -ar 16000 -sample_fmt s16 output_16k_mono.wav
  ```
  - `-ac 1` = 单声道
  - `-ar 16000` = 16kHz 采样率
  - `-sample_fmt s16` = 16bit 位深

### 坑 5：时长字段不严格校验但建议传准

- API 要求传 `duration`（毫秒）
- 实测哪怕传 0 也能上传成功，但官方文档建议传准确值
- WAV 从文件头读：`n_frames / sample_rate * 1000`
- MP3 用 `mutagen.mp3.MP3` 读

---

## 接口坑

### 坑 6：签名算法是老式 HmacSHA1，不是常见的 HmacSHA256

```python
# signa = base64(HmacSHA1(MD5(appid + ts), secretKey))
md5_str = hashlib.md5((appid + ts).encode("utf-8")).hexdigest()  # 32 位小写 hex
mac = hmac.new(secret_key.encode("utf-8"), md5_str.encode("utf-8"),
               digestmod=hashlib.sha1)
signa = base64.b64encode(mac.digest()).decode("utf-8")
```

注意：
- MD5 输出是**32 位小写 hex 字符串**，不是 16 字节 raw bytes
- HMAC 的 key 是 `secretKey`，data 是 MD5 字符串本身（不是其字节）
- 最后做 base64 编码

### 坑 7：参数全部走 URL query，不是 form-data 或 JSON body

- 上传接口：`POST https://raasr.xfyun.cn/v2/api/upload?appId=...&signa=...&ts=...&...`
- `Content-Type: application/json`，但**请求体是音频原始字节流**（不是 JSON）
- 所有业务参数（`fileSize` `fileName` `duration` `language` `pd` `hotWord` …）都在 URL query 里
- 中文热词必须做 URL encode：`quote(hot_word_str, safe='')`

### 坑 8：热词用 `|` 分隔，不是逗号

```python
hot_word_str = "|".join(HOT_WORDS)  # 萨德|爱国者|标准-6|...
params["hotWord"] = hot_word_str
```

限制：
- 最多 200 个热词
- 每个热词 2–16 字
- 整个 hotWord 参数 URL encode 后不能超过某个长度（实测 35 个常规军事术语没问题）

---

## 异步查询坑

### 坑 9：轮询用 POST 不是 GET

- 查询结果接口：`POST https://raasr.xfyun.cn/v2/api/getResult?...`
- 即便没有 body 也是 POST
- 用 GET 会报方法不允许

### 坑 10：status 状态码语义

| status | 含义 |
|---|---|
| 0 | 待处理 |
| 1 | 处理中 |
| 4 | **完成** |
| -1 | 失败 |

完成是 `4`，不是 `2` 也不是 `200`。

### 坑 11：轮询间隔与超时

- 26 分钟音频实测转写耗时约 5 分钟
- 推荐轮询间隔 30 秒（taskEstimateTime 返回的预估值仅供参考）
- 超时上限设 30 分钟比较保险

---

## 结果解析坑

### 坑 12：返回结果是双层嵌套 JSON 字符串

```python
# 外层
data = response.json()
order_result_str = data["content"]["orderResult"]  # 这是 JSON 字符串

# 中间还有一层反斜杠转义
cleaned = re.sub(r"\\\\", r"\\", order_result_str)
outer = json.loads(cleaned)

# 句子在 outer["lattice"] 里
for item in outer["lattice"]:
    inner_str = item["json_1best"]    # 又是 JSON 字符串
    inner = json.loads(inner_str)
    # 句子时间在 inner["st"]["bg"] / inner["st"]["ed"]
    # 句子内容在 inner["st"]["rt"][..]["ws"][..]["cw"][..]["w"]
```

每个 `cw` 是一个字 / 词；要拼接所有 `cw["w"]`（跳过 `wp == "g"` 的空白标记）。

### 坑 13：时间戳单位是毫秒（不是秒、不是百分秒）

- `bg` `ed` 都是毫秒（int）
- 与 B 稿的 `[Nm Ns]` 对齐时记得换算

---

## 业务参数坑

### 坑 14：`pd=mil` 对军事术语显著加分

- 不开 `pd`：默认通用领域，军事术语会被识别成同音常见词
- 开 `pd=mil`：军事领域专用模型介入，配合热词效果叠加

### 坑 15：`eng_smoothproc` + `eng_colloqproc` 让 ASR 接近书面

- `eng_smoothproc=true`：去掉"嗯""那个""呃"
- `eng_colloqproc=true`：口语规整（"咱们"→"我们"等）
- 两个都开后，输出已经非常接近书面文稿
- 但**不会清理"那么""呢""啊""这个"**——这些保留与否需要在融合层决定

---

## 凭证与配额（demo 阶段使用）

```
APP_ID: 84eff996
SECRET_KEY: d37fb0c77e7f4b4f4398e6a8834d1017
免费包: 5 小时（已激活），有效期至 2027 年 5 月
```

> ⚠️ **正式上线时替换为生产凭证**，并把 SECRET_KEY 从代码移到环境变量 / 密钥管理服务。

---

## 一句话总结

讯飞 ASR 标准版**能用、好用**，但接入文档稀少且错误信息误导性强；以上 15 个坑全部踩完后，单次调用的成功率接近 100%。后续接入新 ASR 供应商时，建议按本笔记的结构（选型 / 音频 / 接口 / 异步 / 解析 / 业务参数 / 凭证）做同样的整理。
