OpenWhispr 自定义 ASR Shim 实战:让 Self-Hosted 转录接入任意语音识别后端
【免费下载链接】openwhisprVoice-to-text dictation app with local (Nvidia Parakeet/Whisper) and cloud models (BYOK). Privacy-first and available cross-platform.项目地址: https://gitcode.com/GitHub_Trending/op/openwhispr
本篇技术指南围绕 OpenWhispr 官方示例 custom-asr-shim 展开,讲解如何在不改动 OpenWhispr 源码的前提下,通过一个运行在本机的轻量代理(shim),把 OpenWhispr Self-Hosted 转录模式对接给任何不遵循 OpenAI/audio/transcriptions协议的 ASR 后端(如 StepFun StepAudio、阿里、百度、腾讯及各类企业级语音平台)。读完本文你将掌握 shim 的工作原理、OpenWhispr 侧的完整 HTTP 契约、StepAudio 实战接入方法,以及如何基于模板为任意厂商扩展自己的适配器。
为什么要一个 shim:协议不匹配问题
OpenWhispr 的 Self-Hosted 转录模式假设后端遵循 OpenAI 的线上协议格式:它向{Server URL}/audio/transcriptions发起multipart/form-data的POST请求,并读取返回的 JSON{"text": "..."}。这一点可以从仓库源码得到印证:transcriptionRoute.ts 中buildBatchEndpoint的 fallback 路径就是buildApiUrl(base, "/audio/transcriptions"),而 selfHostedTranscription.js 则定义了transcriptionMode === "self-hosted"且配置了远端 URL 时的启用条件。
然而,大量 ASR API 并不说这种"语言":StepFun StepAudio 2.5、阿里、百度、腾讯以及各种企业级语音平台,各有各的请求结构、音频编码和响应封装。如果直接把 OpenWhispr 指向其中一个,由于线上传输的字节格式对不上,调用会以 400/501 之类的错误失败。
解决方案就是 shim:一个运行在你机器上的小型本地代理,充当"翻译官"。OpenWhispr 继续讲 OpenAI 协议,shim 负责把它翻译成厂商协议再转发,并把厂商响应翻译回 OpenAI 格式。你不需要改动 OpenWhispr 的任何代码。
工作流程与整体架构
官方文档给出了清晰的请求/响应流向:
OpenWhispr --multipart audio--> shim (localhost:8765) --vendor format--> ASR API OpenWhispr <--{"text": "..."}-- shim <--vendor reply-- ASR APIshim 监听/audio/transcriptions(以及/v1/audio/transcriptions)两个路径,用 ffmpeg 把录音转码成厂商需要的格式,调用厂商接口,最后返回 OpenWhispr 期望的 OpenAI 风格 JSON。
从 shim_template.py 的实现看,整个处理管线分为四步(见ShimHandler.do_POST):
- 校验与解析:检查
Content-Length,超过MAX_BODY_BYTES(25 MB)直接返回 HTTP 413;用parse_multipart_form解析multipart/form-data得到文本字段与文件字段; - 临时落盘:按上传文件名后缀(默认
.webm)创建临时文件写入音频字节; - 转码:
convert_audio调用 ffmpeg 统一转码;通用模板转成16 kHz 单声道 WAV,StepAudio 版则转成16 kHz 单声道 s16le 裸 PCM(见 stepaudio_shim.py); - 调用厂商并返回:
transcribe()是唯一需要你改写的函数,其结果包装为{"text": ..., "object": "transcription"}返回 HTTP 200。
无论成功失败,finally块都会清理所有临时文件,不留磁盘残留。
OpenWhispr 发送什么、期待什么:完整契约
官方文档详细列出了 OpenWhispr 侧发出请求的字段,这是编写 shim 时必须严格遵守的契约。
请求:POST {Server URL}/audio/transcriptions,multipart/form-data。
| 字段 | 是否必需 | 说明 |
|---|---|---|
file | 是 | 录音文件。默认是 WebM/Opus,文件名audio.webm;也可能是 ogg/mp4/mp3/wav。发给厂商前需要转码。 |
model | 否 | Self-Hosted 面板中填写的 Model 字符串(若设置了)。要容忍空值或缺失。 |
language | 否 | 仅当选择非 auto 语言时才出现(如en这样的 ISO 代码)。 |
prompt | 否 | 自定义词典提示字符串。 |
几点重要的实现细节:
- 不要从 OpenWhispr 侧要求鉴权:
Authorization: Bearer <key>不属于 Self-Hosted 面板的功能。厂商 API key 应放在 shim 内部的环境变量中(见下文STEP_API_KEY)。这与 shim_template.py 中的API_KEY = os.environ.get("SHIM_API_KEY", "")设计一致——模板本身也只提供可选的SHIM_API_KEY,而不是要求 OpenWhispr 传 key。 - 只走批量 HTTP:Self-Hosted 转录是批量 HTTP 模式,OpenWhispr 不会在此路径发送
stream=true,因此 shim 应返回单个 JSON 对象,而不是 SSE 流——即使上游厂商是流式的。模板中的_send_json实现正是固定返回单对象 JSON。 - 响应约定:HTTP 200 + JSON
{"text": "..."}。OpenWhispr 读取.text字段;文本为空或缺失时,用户端会显示 "No audio detected";非 200 状态则作为 API 错误呈现。
从源码侧印证:transcriptionRoute.ts中language取自preferredLanguage("auto"时不携带),而模型字段在 Self-Hosted 模式下由resolveSelfHostedTranscriptionModel解析,未配置时即为 null——这正是字段表里"可选"的原因。
HTTP 与 HTTPS 的豁免规则
OpenWhispr 对自定义端点强制要求 HTTPS,但对私有与回环主机网开一面。跑在http://localhost:8765的 shim 可以走纯 HTTP;而部署在公网主机上的 shim 必须使用 HTTPS(放在 TLS 反向代理后面,或在 shim 内终止 TLS)。
官方文档明确列出允许纯http://的主机清单:
localhost以及任何*.local主机名127.0.0.0/8(回环)以及 IPv6::110.0.0.0/8、172.16.0.0/12、192.168.0.0/16(私网段)169.254.0.0/16(链路本地)以及 IPv6fe80::/10100.64.0.0/10(CGNAT)- IPv6
fc00::/7(唯一本地地址)
这一规则在源码中有精确对应:urlUtils.ts 的isPrivateHost实现了完全相同的判定逻辑——包括对 IPv4 字面量做严格 dotted-quad 解析以防止127.example.com之类的字符串前缀绕过(见parseIPv4Literal,它拒绝前导零和超过 255 的八位组),以及isSecureHttpEndpoint中https:或http:+ 私有主机的放行规则。值得注意的额外细节:源码还把 Tailscale MagicDNS(.ts.net后缀)视为私有主机,因为其解析到仅限 tailnet 内可达的 CGNAT 地址。
前置条件与快速启动
前置条件:
- Python 3.8 或更新版本(仅用标准库,无需 pip 安装任何依赖)
- PATH 中有 ffmpeg
通用模板启动(填入transcribe()函数后):
python3 shim_template.pyStepAudio 2.5 启动:
export STEP_API_KEY=sk-... python3 stepaudio_shim.pyWindows PowerShell 下:
$env:STEP_API_KEY = "sk-..." python stepaudio_shim.py注意:StepAudio shim 默认以中文转写(language: zh),并忽略 OpenWhispr 转发过来的语言;如需其他语言,直接修改 stepaudio_shim.py 里的language值。
然后在 OpenWhispr 中配置:
- 打开Settings → Transcription
- 选择Self-Hosted
- 将Server URL设为
http://localhost:8765 - 可选地设置Model(会作为
model字段透传;StepAudio shim 会忽略它)
StepAudio shim 从环境变量读取STEP_API_KEY,而不是从 OpenWhispr 读取。这也是 stepaudio_shim.py 启动时校验的:若环境变量未设置,会直接打印错误并退出。
StepAudio 2.5 实现剖析
stepaudio_shim.py 是模板的一个完整工作示例,其关键差异点值得研究:
转码输出裸 PCM:StepAudio 需要的是裸 PCM 而非 WAV 容器,因此convert_audio使用-f s16le输出 16 kHz 单声道 16 位小端 PCM(-ar 16000 -ac 1)。
请求封装:transcribe()把 PCM 读取后 base64 编码,构造一个 JSON payload 发送到https://api.stepfun.com/v1/audio/asr/sse:
payload = { "audio": { "data": audio_b64, "input": { "transcription": { "model": "stepaudio-2.5-asr", "language": "zh", "enable_itn": True, }, "format": { "type": "pcm", "codec": "pcm_s16le", "rate": 16000, "bits": 16, "channel": 1, }, }, } }请求头携带Authorization: Bearer <STEP_API_KEY>、Content-Type: application/json和Accept: text/event-stream,超时设为REQUEST_TIMEOUT = 120秒。
SSE 解析:StepAudio 通过 SSE 流式返回结果,parse_sse_transcript负责解析:累积transcript.text.delta事件的delta片段,优先采用transcript.text.done事件的完整text,当事件名缺失时回退到 data 载荷内的type字段,并跳过[DONE]标记与无法解析的 JSON 块。
错误处理:HTTPError会把状态码与响应体详情一并抛出,URLError抛出底层原因,统一由ShimHandler包装为 500 错误返回给 OpenWhispr。
适配到其他厂商:只改一个函数
官方文档给出的扩展方法是:复制 shim_template.py,然后只替换transcribe()这一个函数为你的厂商调用。模板文件中transcribe的签名如下:
def transcribe(audio_path: str, model: str, language: str | None, prompt: str | None) -> str: """The ONLY function you edit. Call your vendor's ASR backend with the WAV at `audio_path` and return the transcript as a plain string.""" raise NotImplementedError(...)其余一切——HTTP 服务器、multipart 解析器、ffmpeg 转码、体积上限守卫、临时文件清理、响应格式——都已经与 OpenWhispr 的期望匹配,你只需要碰那个跟后端对话的函数。model/language/prompt三个参数直接来自 OpenWhispr 的 UI,可以选择使用也可以忽略。
模板的设计也考虑了健壮性,值得复用:
- 文件大小守卫:超过
MAX_BODY_BYTES(25 MB)返回 413; - 缺失
file字段返回 400,路径不对返回 404; - ffmpeg 不在 PATH 时返回 500 并给出明确错误信息;
except Exception兜底"surface, never swallow",确保任何异常都能反馈给 OpenWhispr 而不是静默丢失。
测试验证
test_shim.py 是标准库 unittest 编写的测试套件,无需网络、无需 ffmpeg——HTTP 测试通过 monkeypatch 掉convert_audio和transcribe,从而在隔离环境中验证请求处理、multipart 解析和响应形状。运行方式:
python3 test_shim.py或:
cd examples/custom-asr-shim && python3 -m unittest测试覆盖了以下关键场景(这也是理解 shim 契约边界的绝佳教材):
- multipart 解析器:二进制内容内嵌 CR/LF 的往返保真(防过度剥离)、UTF-8 文本字段(含
caffè, naïve, 日本語)、缺失 boundary 抛错、boundary 后带 RFC 合法尾随空格、无file字段的情况;解析器在两个模块中是逐字复制的,测试对两份拷贝都跑; - StepAudio SSE 解析器:优先采用
done完整文本而非拼接delta、无done时拼接 delta、type字段回退、跳过[DONE]和畸形 JSON、空流返回空串; - HTTP 端点:合法请求在两个路径(
/audio/transcriptions与/v1/audio/transcriptions)都返回 200 与{"text": "hello world", "object": "transcription"},且transcribe()收到的字节与上传字节完全一致、model/language透传正确;错误路径返回 404、缺file返回 400、超限返回 413。
测试的_HandlerHarness会把 handler 起在临时端口上、静默访问日志、并在结束时恢复被 patch 的全局,展示了如何干净地对http.serverhandler 做单元测试。
小结
OpenWhispr 的 Self-Hosted 转录模式只认 OpenAI 的/audio/transcriptions协议,而现实世界中的 ASR 厂商协议五花八门。这个社区示例用不足三百行的纯标准库 Python 搭建了一座可靠的"协议桥":OpenWhispr 侧零改动、密钥不出本机、临时文件自动清理,且扩展点收敛到一个函数。无论你要接 StepFun StepAudio、阿里、百度、腾讯,还是企业内部语音平台,遵循本文的契约与模板路径即可快速落地;遇到边界行为拿不准时,test_shim.py 就是契约行为的最精确说明书。
说明:本示例基于社区贡献者 ErogosZhou 的原始 StepAudio shim 移植而来,对应 OpenWhispr 的 issue #972,详见 examples/custom-asr-shim/README.md 的 Credit 一节。
【免费下载链接】openwhisprVoice-to-text dictation app with local (Nvidia Parakeet/Whisper) and cloud models (BYOK). Privacy-first and available cross-platform.项目地址: https://gitcode.com/GitHub_Trending/op/openwhispr
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考