
1. 先定位 api.py 的报错层设备、model_dir 还是 torchaudioSenseVoiceSmall 的 API 起不来TaoToken 的 Key 这样喂给 Codex 再查。看到这个标题点进来的多半已经试过官方那份 api.py也照着「复制到模型目录、替换 API.PY、python api.py 启动」的步骤做过一遍结果服务没起来只剩一个看不懂的 traceback。原文说得很简单把 api.py 复制到模型目录python api.py 就能启动。实际跑起来SENSEVOICE_DEVICE、model_dir、torchaudio 这三个位置随便哪一处和你的环境对不上服务都起不来。与其对着报错盲改不如先到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 拿一把 Key把 Codex 配到 TaoToken 的 https://taotoken.net/api 上然后把 api.py 和完整报错一起贴给 Codex让它逐行对照排查。1.1 官方文档没写清楚的那一步原文说「关于 API 启动官网和很多文章都是没有清楚的说明」这句话我深有同感。它给出的 api.py 有三处默认值SENSEVOICE_DEVICE默认cuda:0model_dir默认iic/SenseVoiceSmalltorchaudio.load直接读 BytesIO。这三处在作者的机器上可能一切正常换到你的机器上就变成三个报错源。最麻烦的是官方模型目录下的 README 讲的是模型推理不是 API 服务所以很多人卡在启动阶段根本不知道该往哪查。1.2 为什么建议先配 Codex 再看代码人工排查这三个点需要同时翻 FunASR 源码、torchaudio 文档和 PyTorch 设备管理文档来回确认很容易晕。用 Codex 更快但有一个前提要说清Codex 不会直接连接你的模型目录也不会替你在生产机器上跑python api.py。它能做的是读你贴过去的 api.py 和 traceback帮你比较 model.py 的接口定义然后给出修改建议。实际运行、换设备、重新启动这些动作仍然由你在本地完成再把结果贴回对话。这样分工既安全又能把排查时间压得很短。这三类报错还有一个共同特征它们都不会立刻出现在 API 请求阶段而是出现在进程启动阶段。也就是说你还没碰到 FastAPI 的 /docs 页面程序就已经退出了。判断依据很简单看python api.py退出前最后几行输出。如果是 torch 相关的报错优先看设备如果是 model 或模型文件相关的报错优先看路径如果是导入阶段的段错误优先看依赖版本和 import 顺序。先把判断顺序定下来后面贴给 Codex 时你也能更清楚它为什么先问设备信息。2. 排障前把 Codex 的 Base URL 指到 TaoToken2.1 到 TaoToken 拿 Key打开 TaoToken注册后进入控制台创建一个 API Key。这个 Key 是给 Codex 用的凭据不是给 SenseVoiceSmall 用的。TaoToken 本身是统一 API 通道负责把 Codex 的请求路由到对应模型不参与 SenseVoiceSmall 的推理也不接触你的音频文件或模型目录。创建完成后Key 会以sk-...的形式显示先把它保存到终端环境变量里后续 Codex 配置会读取它export TAOTOKEN_API_KEYYOUR_API_KEY2.2 填进 Codex 的 Base URLCodex CLI 的配置文件在~/.codex/config.toml。这里必须分清两个地址注册、创建 Key、看模型广场都去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 填进 Codex 的请求地址是 https://taotoken.net/api末尾不要加/v1。模型 ID 不写死以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场当时列出的 ID 为准。config.toml 这样写model 你的模型ID以TaoToken模型广场为准 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY保存后先做一次连通性验证直接问 Codexfrom model import SenseVoiceSmall这行导入可能触发哪些异常它能正常回答说明 Key、Base URL、模型 ID 三条配置都通了。注意这里是 Codex 场景不要往 config.toml 里写ANTHROPIC_BASE_URL那套变量Codex 只认model_provider和base_url。如果你在验证连通性时遇到连接错误先检查 base_url 是不是写成了 https://taotoken.net/api/v1TaoToken 的接口地址本身已经包含版本路由再确认环境变量名和 config.toml 里的 env_key 完全一致大小写不一致会导致鉴权失败。3. 把 api.py 和报错贴给 Codex三个高频启动错误这样查3.1 SENSEVOICE_DEVICE 的坑原文第一行注释写着export SENSEVOICE_DEVICEcuda:1在单卡机器上cuda:1根本不存在。实测下来这类报错通常出现在模型加载阶段traceback 会指向SenseVoiceSmall.from_pretrained(modelmodel_dir, device...)。Codex 看到这种错误第一反应是让你确认本机 GPU 数量和 torch 是否可用。你可以先跑下面这段把输出原样贴给 Codeximport torch print(torch.cuda.is_available()) print(torch.cuda.device_count()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only)Codex 会根据device_count()的结果建议你改用cuda:0或者先用export SENSEVOICE_DEVICEcpu验证接口逻辑。CPU 推理慢一点但能把「设备不存在」和「模型加载失败」两类问题拆开后续再切回 GPU 会更有方向。3.2 model_dir 和 torchaudio 的加载顺序model_dir iic/SenseVoiceSmall看起来像本地路径其实是 Hugging Face 上的模型仓库名。第一次运行时会联网下载网络不好就卡在进度条网络被限制时直接抛连接错误。Codex 通常建议先检查本地模型目录是否存在不存在就手动下载然后把 model_dir 改成绝对路径避免每次启动都去判断是网络问题还是加载问题。另一个容易踩的是 import 顺序原文把import torchaudio放在from model import SenseVoiceSmall之前某些 funasr 版本在 torchaudio 导入阶段会初始化 libtorch 的 so 文件顺序反了可能出现OMP: Error #15或段错误。Codex 判断这类问题的方式很简单看 traceback 最后一行发生在哪个导入语句再决定调整顺序而不是凭感觉把 import 挪来挪去。3.3 一次性把上下文喂给 Codex只贴一行RuntimeError: No such file or directory没法定位。Codex 需要的东西有四样你执行的命令是python api.py还是/path/to/api.py完整 traceback 前 20 行到后 20 行torch、torchaudio、funasr 的版本号跑这个项目用的是 conda 环境还是 venv。把这些粘进对话后Codex 会先判断是 import 阶段还是启动阶段出错再判断是设备还是依赖问题最后给出针对性修改。这个过程和人工排查的思路一致区别是它不需要你在 FunASR 源码里逐行翻。4. 按 Codex 的修改意见改成可启动的 api.py4.1 替换前的备份和改动清单原文说「直接复制到模型目录下替换 API.PY 即可」这个动作没问题但替换前先备份cp api.py api.py.bak然后把 Codex 的修改意见逐条落进文件。根据前面三类高频报错改动清单通常包含设备选择改为自动判断model_dir 支持从环境变量传入torchaudio.load 失败时降级到临时文件重试。这三处改动分别对应第 3 节的三类报错设备回退解决 cuda:1 越界问题model_dir 环境变量解决模型下载路径不确定问题torchaudio 降级重试解决 BytesIO 解析失败问题。Codex 给出的修改意见通常不会超过这三类因为启动 API 的关键路径就那么长从 import 到 from_pretrained再到 uvicorn.run。把每一段的假设都写成可检查的代码启动报错就能被快速收敛。提示不要在原文件上直接改完就删掉备份。Codex 给的修改意见不一定一次到位保留.bak可以方便对照原始逻辑。4.2 改后的 api.py 长什么样下面这份 api.py 保留了原文的 FastAPI 路由、Language 枚举和 rich_transcription_postprocess 处理逻辑只加了设备回退、模型路径检查、音频加载容错三处改动import os import re import sys from pathlib import Path from io import BytesIO from enum import Enum from typing import List import torch import torchaudio from fastapi import FastAPI, File, Form from fastapi.responses import HTMLResponse from typing_extensions import Annotated from model import SenseVoiceSmall from funasr.utils.postprocess_utils import rich_transcription_postprocess class Language(str, Enum): auto auto zh zh en en yue yue ja ja ko ko nospeech nospeech def pick_device(preferred: str) - str: if preferred.startswith(cuda): idx int(preferred.split(:)[1]) if : in preferred else 0 if torch.cuda.is_available() and idx torch.cuda.device_count(): return preferred if torch.cuda.is_available(): return cuda:0 return cpu device pick_device(os.getenv(SENSEVOICE_DEVICE, cuda:0)) model_dir os.getenv(SENSEVOICE_MODEL_DIR, iic/SenseVoiceSmall) if not Path(model_dir).exists(): print(fmodel_dir {model_dir} 不存在将尝试在线加载, filesys.stderr) m, kwargs SenseVoiceSmall.from_pretrained(modelmodel_dir, devicedevice) m.eval() regex r\|.*\| app FastAPI() app.get(/, response_classHTMLResponse) async def root(): return !DOCTYPE html html headmeta charsetutf-8titleApi information/title/head bodya href/docsDocuments of API/a/body /html app.post(/api/v1/asr) async def turn_audio_to_text( files: Annotated[List[bytes], File(descriptionwav or mp3 audios in 16KHz)], keys: Annotated[str, Form(descriptionname of each audio joined with comma)], lang: Annotated[Language, Form(descriptionlanguage of audio content)] auto, ): audios [] audio_fs 0 for file in files: file_io BytesIO(file) try: data, audio_fs torchaudio.load(file_io) except RuntimeError: tmp /tmp/sensevoice_input.wav with open(tmp, wb) as f: f.write(file) data, audio_fs torchaudio.load(tmp) if data.size(0) 1: data data.mean(0, keepdimTrue) audios.append(data.squeeze(0)) file_io.close() if lang : lang auto if keys : key [wav_file_tmp_name] else: key keys.split(,) res m.inference( data_inaudios, languagelang, use_itnTrue, ban_emo_unkFalse, keykey, fsaudio_fs, **kwargs, ) if len(res) 0: return {result: []} for it in res[0]: it[raw_text] it[text] it[clean_text] re.sub(regex, , it[text], 0, re.MULTILINE) it[text] rich_transcription_postprocess(it[text]) return {result: res[0]} if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)这里有两处容易被忽略的细节pick_device会把cuda:1自动回退到cuda:0或 CPUmodel_dir改成可通过SENSEVOICE_MODEL_DIR指定本地绝对路径torchaudio 加载失败时先落到临时文件再试一次。正则部分也恢复成|.*|避免 HTML 转义实体进入匹配串。Codex 给你指出问题最终这一步修改由你自己完成正好符合安全边界。5. 启动后回 TaoToken 控制台对一次调用记录5.1 本地执行 python api.py 并验证接口把改好的 api.py 放进 SenseVoiceSmall 模型目录后先确认设备变量export SENSEVOICE_DEVICEcuda:0 # 没有 GPU 就改成 cpu python api.py看到Uvicorn running on http://0.0.0.0:8000才算服务真正起来。这时打开 http://127.0.0.1:8000/docs 能看到 FastAPI 自带的文档页。原文说「启动 API 无需启动 SenseVoice 模型本身」这句话成立的前提是 api.py 已经成功加载了 model.py 里的 SenseVoiceSmall所以见到 Uvicorn 日志就说明模型加载已经通过如果模型加载失败报错会停在from model import SenseVoiceSmall那一行Uvicorn 根本没有机会运行。5.2 用 curl 跑通一次识别请求另开一个终端准备一个 16KHz 的 wav 文件执行curl -X POST http://127.0.0.1:8000/api/v1/asr \ -F filestest.wav \ -F keystest \ -F langzh返回的 JSON 里如果带text字段整条链路就通了。如果你手头没有 16KHz 的 wav可以用 ffmpeg 先转一个ffmpeg -i input.mp3 -ar 16000 -ac 1 test.wav这一步同样在本地执行Codex 不会替你做。如果返回空列表或者报错把 curl 的输出和 api.py 的启动日志一起贴回给 Codex继续对照rich_transcription_postprocess的处理逻辑排查。Codex 只能看到你贴给它的文本日志里是CUDA out of memory、No such file or directory还是OMP: Error #15会直接影响它给出的方向所以日志越完整越好。5.3 确认 Codex 的排查调用已经记账Codex 帮你排查 api.py 的过程中每一次提问都会消耗 Token。排查告一段落后回到 TaoToken 模型对话 用同一把 Key 发一条测试消息确认调用被正常记录。如果这种排障场景以后会经常出现打开 Coding Plan 看套餐是否合适Key 的管理入口在 控制台 API Keys。以后再遇到 SenseVoiceSmall 启动报错先判断报错属于设备、路径还是依赖层再让 Codex 对比你贴过去的日志通常比反复搜索更快得出改法。