一句话总结:控制台能跑 ≠ 你后端能跑。前端 WebM 录音 → 百炼新平台识别,中间隔着格式、模型、SDK 三座大山,爬过去你就赢了。

前言:一周的崩溃,差点把键盘砸了

做 HIS 系统开发这么多年,自认为各种第三方服务接入也算轻车熟路了。结果在阿里云百炼的新平台上搞语音识别,整整踩了一周的坑——从 “这不就是个 API 调用吗” 到 “这玩意儿到底能不能用” 的心态转变,只用了两天。

最让人崩溃的是什么?控制台上传同一个 WebM 文件,用 fun-asr 模型能正常识别,返回结果清清楚楚。 然后我信心满满地把同一个文件塞给后端接口——失败、失败、还是失败。

每次看到控制台那个绿色的识别结果,再看看自己终端里红色的报错,就一个感觉:它在嘲讽我。

这篇文章记录了我从 Transcription 异步转写到 Recognition 实时流,从自己拼 HTTP 请求到最终靠 ffmpeg 转码 + SDK 正确调用,一步步踩坑、一步步破案的全过程。如果你也在用 FastAPI + 阿里云百炼做语音识别,希望能帮你少踩几个坑。


一、先搞懂:为什么控制台能跑,我后端跑不通?

核心问题:格式 / 模型 / SDK 三者不匹配

控制台看起来简单——上传文件,点一下,出结果。但它在底层偷偷帮你做了很多事:

1
2
控制台底层流程:
WebM 音频 → 自动转码(WAV/PCM 16kHz)→ 上传到内部 OSS → 调用识别服务 → 返回结果

而我一开始的链路是这样的:

1
2
我的链路(错误版本):
WebM 音频 → 直接塞给 API → ❌ 格式不支持

控制台上传 WebM 能识别,是因为它帮你转了码,不是因为它支持 WebM。 这个认知偏差,是我踩掉所有坑的根源。

整个问题的本质其实就是一张匹配表:

环节 你的输入 百炼需要什么 匹配吗?
音频格式 WebM (Opus) WAV / PCM 16kHz 单声道
调用方式 自己拼 HTTP / Transcription 类 Recognition 类(新平台)
模型名称 fun-asr fun-asr-realtime

三个关键环节,我一个都没对上。下面按时间顺序复盘每一个坑。


二、我踩过的所有坑(按顺序复盘)

坑1:Transcription + OSS 上传,FILE_403_FORBIDDEN

症状:

一开始我看文档,发现有 Transcription 类,想着异步转写正好,传个文件 URL 过去慢慢等结果就行。配合 OSSUtils.upload 把文件传到 OSS,拿到 URL 传给 Transcription API。

然后开始了漫长的等待——任务状态永远是 PENDING,过了几分钟变成 FAILED。错误信息先是 FILE_403_FORBIDDEN,后来又冒出 SERVER_ERROR

根因:

两个问题叠在一起:

  1. 权限问题: OSSUtils.upload 上传的文件默认是私有权限,百炼的服务账号根本没有权限读取你这个文件。API 拿到 URL 去下载,直接 403。
  2. 平台兼容问题: 更关键的是,Transcription 类是旧平台的异步转写方案,和新百炼平台的权限体系、模型体系根本不兼容。你就算把文件设成公开读,它也不一定能正常跑。

教训:

在百炼新平台上,别碰 Transcription 类和 OSSUtils.upload 这套组合拳。这条路是死胡同,官方文档里也没有明确告诉你”新平台请用 Recognition”,我是踩完了才发现。


坑2:自己拼 HTTP POST 请求,URL 和格式全错

症状:

既然 SDK 的 Transcription 不行,那我就直接拼 HTTP 请求——反正就是个 POST 嘛,我自己拼 body,Base64 传音频,总不会有问题吧?

结果:

1
2
3
url error: ...
InvalidParameter: ...
task can not be null

换了 N 个 API 地址,改了 N 版请求体,永远是这几个错误在轮播。

根因:

  1. API 地址一直在变: 新平台刚上线不久,API 地址迭代很快。网上的教程、博客里的地址基本都是旧的,贴进去直接 url error
  2. fun-asr 的入参格式和其他模型不一样: 百炼有好多模型(Paraformer、Whisper、fun-asr 等等),每个模型的请求体格式都有差异。我照着某个通用文档拼的 body,塞给 fun-asr 模型,格式根本对不上。
  3. Signature 鉴权自己算也容易出错: SDK 帮你处理好了鉴权,自己拼请求还得手算签名,一个不小心就错。

教训:

不要自己拼 HTTP 请求。新平台老老实实用 SDK,SDK 内部就是 WebSocket 流式发送,和控制台的底层链路一致。你拼半天,人家 SDK 一行 call() 就搞定了。


坑3:Recognition 类用错模型,ModelNotFound

症状:

痛定思痛,决定彻底拥抱 SDK。查到 Recognition 类可以处理文件转写,赶紧试:

1
2
3
4
5
recognition = Recognition(
model="fun-asr", # 控制台看到的就是这个模型名!
...
)
result = recognition.call("my_audio.webm")

报错:

1
ModelNotFound: fun-asr

我整个人傻了——控制台上明明就是这个模型名啊?难不成模型还会隐身?

根因:

Recognition 类有两种工作模式:

模式 支持模型 用途
实时流模式 fun-asr-realtimeparaformer-realtime-v2 真正的实时语音识别,WebSocket 流式推送
文件模式 fun-asr-realtime 等 realtime 系列 用 WebSocket 模拟文件流式发送,实现文件转写

两个模式都只支持带 -realtime 后缀的模型fun-asr 这个模型名是控制台文件上传测试时用的(底层也被转成了实时模型调用),但 SDK 的 Recognition 类不认识它,必须用 fun-asr-realtime

教训:

SDK 里的模型名和控制台显示的模型名是两回事。Recognition 类请认准 fun-asr-realtime,不要直接照抄控制台上的名字。


坑4:Windows 下 ffmpeg 转码,异步子进程报错

症状:

模型名改对了,文件传上去,心想这次总该行了吧?

1
Error: Audio format not supported

好家伙,终于到了最后一个坑——格式问题。前端的 MediaRecorder 录出来的 WebM(Opus 编码),fun-asr-realtime 根本不支持。必须转成 WAV/PCM 16kHz 格式。

于是上了 ffmpeg:

1
ffmpeg -i input.webm -acodec pcm_s16le -ar 16000 -ac 1 -y output.wav

命令行手动跑,完美。用 Python 的 asyncio.create_subprocess_exec 调,挂了

根因:

Windows 下用 asyncio.create_subprocess_exec 调用 ffmpeg,有几个坑:

  1. 路径问题: ffmpeg 可能不在系统 PATH 里,或者 Python 子进程继承的环境变量和你的终端不一样。
  2. Windows 的 ProactorEventLoop 对子进程的支持问题: 在 Windows 上,asyncio 默认的事件循环策略 WindowsProactorEventLoopPolicycreate_subprocess_exec 的 pipe 处理有坑,可能导致进程卡死或者报 NotImplementedError
  3. 大文件处理: 音频文件稍微大一点,stdout/stderr 的 pipe 缓冲区满了,子进程直接卡住。

解决方法是不用 asyncio 子进程,改成 subprocess.run 同步执行,反正转码本身就几秒钟的事,用 run_in_executor 丢到线程池里就行,别跟 asyncio 子进程较劲。


三、最终成功的完整方案

架构总览

1
2
3
前端 MediaRecorder → WebM Blob → POST /api/speech-to-text

后端 FastAPI → 保存临时文件 → ffmpeg 转码 → Recognition 调用 → 清理临时文件 → 返回文本

1. ffmpeg 转码逻辑(WebM → WAV/PCM 16kHz)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import subprocess
import tempfile
import os
from pathlib import Path


def convert_webm_to_wav(webm_path: str) -> str:
"""
将 WebM 音频转码为 WAV/PCM 16kHz 单声道。
使用 subprocess.run 同步执行,避免 Windows 下 asyncio 子进程的坑。

Args:
webm_path: 输入 WebM 文件路径

Returns:
输出 WAV 文件路径

Raises:
RuntimeError: ffmpeg 转码失败
"""
wav_path = webm_path.rsplit(".", 1)[0] + ".wav"

cmd = [
"ffmpeg",
"-i", webm_path,
"-acodec", "pcm_s16le", # PCM 16-bit 小端
"-ar", "16000", # 采样率 16000 Hz
"-ac", "1", # 单声道
"-y", # 覆盖已存在的输出文件
wav_path,
]

result = subprocess.run(cmd, capture_output=True, text=True)

if result.returncode != 0:
raise RuntimeError(
f"ffmpeg 转码失败: {result.stderr}"
)

return wav_path

2. Recognition 类正确调用 fun-asr-realtime 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os
from dashscope.audio.asr import Recognition, RecognitionResult


def recognize_audio(wav_path: str) -> str:
"""
调用百炼新平台 Recognition 类,用 fun-asr-realtime 模型识别音频。

Args:
wav_path: WAV/PCM 16kHz 单声道音频文件路径

Returns:
识别出的完整文本

Raises:
RuntimeError: 识别失败
"""
# 确保 API Key 已设置(从环境变量读取)
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
raise RuntimeError("请设置环境变量 DASHSCOPE_API_KEY")

recognition = Recognition(
model="fun-asr-realtime", # 注意:必须带 -realtime 后缀!
)

result: RecognitionResult = recognition.call(wav_path)

if result.get_sentence() is None:
raise RuntimeError(f"识别失败: {result.get_last_error()}")

# 获取完整识别文本
text = result.get_sentence().get_text()
return text

3. 完整的 FastAPI 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import os
import uuid
import logging
import tempfile
from pathlib import Path
from contextlib import asynccontextmanager

from fastapi import FastAPI, UploadFile, File, HTTPException
from pydantic import BaseModel

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


# ---------- 响应模型 ----------
class STTResponse(BaseModel):
success: bool
text: str | None = None
error: str | None = None


# ---------- FastAPI 应用 ----------
app = FastAPI(title="语音识别服务")


@app.post("/api/speech-to-text", response_model=STTResponse)
async def speech_to_text(file: UploadFile = File(...)):
"""
接收前端 WebM 音频,转码后调用百炼识别,返回文本。
"""
tmp_webm = None
tmp_wav = None

try:
# 1. 校验文件类型
if not file.filename or not file.filename.endswith(".webm"):
raise HTTPException(
status_code=400,
detail="仅支持 WebM 格式音频",
)

# 2. 保存上传的 WebM 到临时文件
suffix = ".webm"
with tempfile.NamedTemporaryFile(
delete=False, suffix=suffix
) as f:
content = await file.read()
f.write(content)
tmp_webm = f.name

logger.info(f"收到音频文件: {file.filename}, 大小: {len(content)} bytes")

# 3. ffmpeg 转码(丢到线程池避免阻塞事件循环)
import asyncio
from functools import partial

loop = asyncio.get_running_loop()
wav_path = await loop.run_in_executor(
None, partial(convert_webm_to_wav, tmp_webm)
)
tmp_wav = wav_path
logger.info(f"转码完成: {wav_path}")

# 4. 调用百炼识别
text = await loop.run_in_executor(
None, partial(recognize_audio, wav_path)
)
logger.info(f"识别结果: {text}")

return STTResponse(success=True, text=text)

except HTTPException:
raise
except Exception as e:
logger.exception("语音识别流程异常")
return STTResponse(success=False, error=str(e))

finally:
# 5. 清理临时文件
for tmp_file in (tmp_webm, tmp_wav):
if tmp_file and os.path.exists(tmp_file):
try:
os.unlink(tmp_file)
except OSError:
pass

安装依赖

1
2
3
4
5
6
pip install dashscope fastapi uvicorn python-multipart

# Windows 上安装 ffmpeg(用 winget 或 chocolatey)
winget install ffmpeg
# 或
choco install ffmpeg

启动服务

1
uvicorn main:app --reload --host 0.0.0.0 --port 8000

前端只需用 MediaRecorder 录好 WebM,fetch 发一个 FormData 过来就行:

1
2
3
4
5
6
7
8
9
10
const formData = new FormData();
formData.append("file", webmBlob, "recording.webm");

const res = await fetch("/api/speech-to-text", {
method: "POST",
body: formData,
});

const data = await res.json();
console.log(data.text); // "喂,你好,你是谁?"

听到那句 「喂,你好,你是谁?」 从终端打印出来的时候,我差点哭出来。一周的坑,终于走通了。


四、避坑总结(干货,抄作业用)

1. 格式 / 模型 / SDK 严格匹配表

你的场景 前端格式 需要转码? 用哪个 SDK 类? 模型名
浏览器录音 → 识别 WebM (Opus) ✅ 必须转 WAV/PCM 16kHz Recognition fun-asr-realtime
已有 WAV 文件 → 识别 WAV/PCM 16kHz Recognition fun-asr-realtime
实时麦克风流 → 识别 PCM 流 Recognition (实时流模式) fun-asr-realtime

2. 前端 WebM 必须转码,别指望模型直接吃

前端的 MediaRecorder 产出的 WebM(Opus 编码),百炼的实时模型不直接支持。控制台上传 WebM 能识别是因为它偷偷帮你转了码。你的后端必须自己做这一步。

转码参数记住这组 “黄金配置”:

1
ffmpeg -i input.webm -acodec pcm_s16le -ar 16000 -ac 1 -y output.wav
  • pcm_s16le:16-bit PCM,小端序
  • -ar 16000:16kHz 采样率
  • -ac 1:单声道

3. Windows 下 ffmpeg 转码的正确姿势

  • 不要用 asyncio.create_subprocess_exec,用 subprocess.run + loop.run_in_executor
  • 确保 ffmpeg 在 PATH 里,或者用绝对路径
  • 不要忘记 -y 参数,否则输出文件已存在时 ffmpeg 会交互式询问,进程直接挂起

4. 百炼新平台 SDK 使用速查

需求 关键参数 备注
音频文件转文字 dashscope.audio.asr.Recognition model="fun-asr-realtime" 文件模式,SDK 内部 WebSocket 流式发送
实时麦克风转文字 dashscope.audio.asr.Recognition model="fun-asr-realtime" 实时流模式,传 PCM 数据
异步转写 dashscope.audio.asr.Transcription 新平台别用,不兼容

5. 心态避坑

控制台能跑 ≠ 后端能跑。

控制台是”演示模式”,很多脏活累活(转码、格式转换、权限处理)它帮你做掉了。你后端调 API 是”生产模式”,每个环节都得自己兜。


结语

如果你也在做 HIS 系统或者类似的医疗信息化产品,大概率跟我一样——后端 Java/Python 一把梭,前端能跑就行,语音识别这种”看起来调个 API 就行”的东西,实际落地却一堆坑。

希望这篇文章能帮你少走弯路。起码下次你在百炼控制台看到识别结果正常、后端却报错的时候,能想起这篇文章,而不是像我一样对着屏幕怀疑人生。

最后附上我的博客链接:

欢迎来踩,一起吐槽。