699 字
3 分钟
语音识别与降级链路
语音识别与降级链路
本篇聚焦项目里的语音识别模块。它的核心并不只是“调用一个 ASR 接口”,而是如何在同一条链路里处理 Data URI、同步主方案、异步降级方案,并保证失败时不让整个聊天流程崩掉。
函数签名
def transcribe_audio_to_text(audio_path: str) -> str: """ 将本地音频文件转为文字。 失败时自动降级,保证程序不崩溃。 """完整执行流程
第一步:Base64 编码
# 为什么要 Base64 编码?# 音频是二进制数据,HTTP JSON 只能传文本# Base64 把二进制转为 ASCII 字符串(体积增大约 33%)
ext = Path(audio_path).suffix.lower().lstrip('.') or 'wav'
# 部分后缀到 MIME 子类型的映射fmt_map = {'m4a': 'mp4', 'ogg': 'ogg', 'flac': 'flac', 'mp3': 'mp3'}audio_fmt = fmt_map.get(ext, 'wav')mime_type = f"audio/{audio_fmt}" # 例如 "audio/wav"
with open(audio_path, "rb") as f: audio_b64 = base64.b64encode(f.read()).decode('utf-8')
# Data URI 格式:data:<MIME>;base64,<数据># 这样就能把文件「内嵌」进 JSON,不依赖文件服务器data_uri = f"data:{mime_type};base64,{audio_b64}"Data URI 长什么样?data:audio/wav;base64,UklGRj6yAQBXQVZFZm10IBAAAA...(很长的字符串)
方案一:qwen3-asr-flash
resp = httpx.post( DASHSCOPE_MULTIMODAL_URL, # 原生多模态接口,不是 compatible-mode! headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={ "model": "qwen3-asr-flash", "input": { "messages": [ # system 消息必须存在,但 ASR 模型不支持自定义 prompt,留空 {"role": "system", "content": [{"text": ""}]}, # audio 字段直接放 Data URI 字符串(不是字典!) {"role": "user", "content": [{"audio": data_uri}]}, ] }, "parameters": { "asr_options": { # enable_itn=False:保留口语数字,不转阿拉伯数字 # "三点一四" 不会变成 "3.14" "enable_itn": False } }, }, timeout=30,)
# 原生接口的返回结构(与 OpenAI 兼容接口不同!)result = resp.json()text = result["output"]["choices"][0]["message"]["content"][0]["text"].strip()三大坑:为什么之前一直报错?
错误尝试 报错 正确做法 用 compatible-mode接口发 base64InvalidParameter改用原生多模态接口 "type": "audio_url"+audio_url.urlInput should be 'text','image','audio'...改为 "audio": data_uri"audio": {"data": "...", "format": "wav"}audio: Input should be a valid stringaudio字段直接是字符串,不是字典
方案二:sensevoice-v1(降级)
sensevoice 使用**异步「提交-轮询」**模式:
# ── 第一步:提交任务 ──────────────────────────────────submit_resp = httpx.post( DASHSCOPE_ASR_SUBMIT_URL, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-DashScope-Async": "enable", # ← 必须!否则走同步接口会 404 }, json={ "model": "sensevoice-v1", "input": {"file_url": data_uri}, # sensevoice 也支持 Data URI "parameters": {}, },)task_id = submit_resp.json()["output"]["task_id"]
# ── 第二步:轮询结果 ──────────────────────────────────for attempt in range(POLL_MAX_RETRIES): # 最多等 40 秒 time.sleep(POLL_INTERVAL_SECONDS) poll_resp = httpx.get( DASHSCOPE_TASK_QUERY_URL.format(task_id=task_id), headers={"Authorization": f"Bearer {api_key}"}, ) status = poll_resp.json()["output"]["task_status"]
if status == "SUCCEEDED": text = poll_resp.json()["output"]["results"][0]["transcription"] return text elif status in ("FAILED", "CANCELED"): break # 失败,退出轮询 # PENDING / RUNNING:继续等待API 接口对比
| 维度 | qwen3-asr-flash(主) | sensevoice-v1(降级) |
|---|---|---|
| 接口类型 | 同步,立即返回 | 异步,需轮询 |
| 延迟 | 秒级 | 秒级~十秒 |
| Base64 支持 | ✅ | ✅ |
| URL 路径 | /aigc/multimodal-generation/generation | /audio/asr/transcription |
| 请求头特殊要求 | 无 | X-DashScope-Async: enable |
| 结果路径 | output.choices[0].message.content[0].text | output.results[0].transcription |
相关笔记