docs: correct HTTP migration blog contracts

Signed-off-by: zhifu gao <[email protected]>
This commit is contained in:
zhifu gao
2026-09-09 08:33:29 +00:00
parent dff96b69f1
commit cffe7feaa0
9 changed files with 642 additions and 214 deletions
@@ -24,14 +24,14 @@
"blog/funclip-v2-1-0-video-clipping-release.html": "88f6c44e5332d1746c4db0fc97d755ef82f12e46d8f1c1c0ad9351152ff9dfc1",
"blog/funclip-v2-2-0-moss-speaker-clipping.html": "0d50747d3992301fb3062ad2cdf903bd427bec3db1f1e4fab1f06de207928bc9",
"blog/generate-subtitles-srt-vtt-from-audio-video.html": "f1133235673d441cc654310581f59a319f182314ab7c0b0824af87da9f4a0591",
"blog/index.html": "92af7f61f70b676eb942c889b4501cab5c328a0eb099bad487b9adb9b8d2ef16",
"blog/index.html": "c3edf2411f377beb2bf01a0486349b189b1548f6babfc4794c16ab43f49754b4",
"blog/japanese-speech-recognition.html": "399f5ce84e68ac00854bdf70b52b1cde6795efca7c800fd6492035a37a7c1b68",
"blog/lightweight-speech-recognition-cpu.html": "d6270066222ed5baef0df108e6a4155f4a87bb5482169f23f218794d371f3281",
"blog/meeting-transcript-acceptance.html": "3e35d1f283c7005977ad8cbd079e38c51e14b51f04f50cedb5636e2d4baf38bd",
"blog/punctuation-restoration-python.html": "6cd26e03bf75b4343afd7b351c2681ebd513b058fb549c5888802be9ff3a1229",
"blog/self-hosted-deepgram-assemblyai-alternative.html": "13b4287b5ef0d3159bf0af09ece3bc1fea66f13390b13c7f33b8b5e85351a1f5",
"blog/self-hosted-deepgram-assemblyai-alternative.html": "feb17aae8e0f3fe46914c8725eacc8fcdf7fe78bf22a291347d6107c24e3992e",
"blog/self-hosted-google-aws-azure-speech-to-text.html": "8e4000fb9e2b33a48a3907d4bd0f55db57ba74121a89b5fa12b412c3c963f49a",
"blog/self-hosted-openai-whisper-api-alternative.html": "8e7352392dc755428bd272c208107a64d9c6d5d1700ef52b42553a3ec234e3fd",
"blog/self-hosted-openai-whisper-api-alternative.html": "3c44496e02a15addfe6063b9b8c2d586b3861925701c4fbd81dba77be69c5de9",
"blog/sensevoice-deployment-guide.html": "61c34fb7626444a0f1b5c93013dd95c10c576333791eaca1ab819baf0a8cdad6",
"blog/sensevoice-emotion-language-detection.html": "a00c40bfbaa3f0f390bc00f052b7e4e299a7d193b088014b7515813f1f8a636b",
"blog/speech-to-text-python-transcribe-audio.html": "87335710235dfdec9e01288f03a9a294c7bc3f730110e8d06f2b4c9d4906e284",
@@ -66,14 +66,14 @@
"en/blog/funclip-v2-1-0-video-clipping-release.html": "229baf59adf2c3290541d9b3c8a6243992406ba84b712714e9d14599205cb1d4",
"en/blog/funclip-v2-2-0-moss-speaker-clipping.html": "644159267f0fa4e7aab9d0eda98f68cd2b6365a9985038b639c626a5ea5ea7e8",
"en/blog/generate-subtitles-srt-vtt-from-audio-video.html": "1530d4b9e94820a0b60d801e8d1c19b7aeb031b2f2b76c092657392e47706c6d",
"en/blog/index.html": "d5a5787e0cf9868752e4c94ad4e699f616f6d81b1b9bc9e38332f8690cd2a017",
"en/blog/index.html": "8fccbb7bf21902521cfe78cdf054abf75c5b6fe7ed571f25bfe4a04468c565f8",
"en/blog/japanese-speech-recognition.html": "c476adcc2be1ed7c19e2345cc91b8ee6a0e04efd794b9b6476dd5d9b3a2c8b04",
"en/blog/lightweight-speech-recognition-cpu.html": "0e02c0e0853b12613d32c250f593c05c2052f18231207b63ee01d6e6600a86f7",
"en/blog/meeting-transcript-acceptance.html": "3183682d2cae0ce18f68e3bd33e22b9effb4de699f6a4d821938cc03dd6835bf",
"en/blog/punctuation-restoration-python.html": "d6e30049f48d9dc6e6bc22eb567830013e4161eca51215616738b675dd1ffc40",
"en/blog/self-hosted-deepgram-assemblyai-alternative.html": "73b2fc101101fd2408ed49fdb2b0995ada59dec0016a91ed49efb856ebb8de53",
"en/blog/self-hosted-deepgram-assemblyai-alternative.html": "4239a93246726c6f010a2fb20208211e050792a372e3ea356d1a93ac028a8a96",
"en/blog/self-hosted-google-aws-azure-speech-to-text.html": "4a8453451722b3ec8735224a586744ddfbfec9458cedd433f9fc908351027e9a",
"en/blog/self-hosted-openai-whisper-api-alternative.html": "cda3b8e45857996031b888b2bf076666b7c3dcae355f097e942fd770a3f0b729",
"en/blog/self-hosted-openai-whisper-api-alternative.html": "3e004bdf307bfb25fe2261554f130077678ca99b58f9dfe0e48b1ecae5547c19",
"en/blog/sensevoice-deployment-guide.html": "8882912c6a84ec0a0e025d0eff8fcfa74bf76324bd4c9cae862c43c87c6e10b3",
"en/blog/sensevoice-emotion-language-detection.html": "4fa0d49d34df3b923323fc49fbb02c432efd7cd0d90d81e84621dbbecc08cd29",
"en/blog/speech-to-text-python-transcribe-audio.html": "4fc57aef3c6177e28afa26fa6c41f67280b986e384b32a63a2f79ca67bcfe631",
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="zh"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>自托管替代 Deepgram / AssemblyAI:开源免费的语音识别 API | FunASR 博客</title><meta name="description" content="想替代 Deepgram/AssemblyAI 这类按分钟付费的云语音识别 API?FunASR 开源免费、可自托管、音频不出本地、中文领先,自带 OpenAI 兼容 /v1/audio/transcriptions,客户端只改 base_url 即可迁移。"><meta name="keywords" content="Deepgram 替代, AssemblyAI 替代, 自托管语音识别 API, 开源语音转文字 API, 替代云语音识别, 私有化语音识别, OpenAI 兼容语音识别, FunASR"><link rel="icon" href="/logo.png">
<meta property="og:title" content="自托管替代 Deepgram / AssemblyAI:开源免费的语音识别 API | FunASR 博客"><meta property="og:description" content="想替代 Deepgram/AssemblyAI 这类按分钟付费的云语音识别 API?FunASR 开源免费、可自托管、音频不出本地、中文领先,自带 OpenAI 兼容 /v1/audio/transcriptions,客户端只改 base_url 即可迁移。"><meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<title>从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收 | FunASR Blog</title><meta name="description" content="评估自托管转写迁移:映射请求和结果契约,检查鉴权、返回格式、模型质量与运维成本。提供 FunASR 本地 JSON 示例,不承诺厂商 API 或 SDK 直接替换。"><meta name="keywords" content="Deepgram 替代, AssemblyAI 替代, 自托管语音识别 API, 开源语音转文字 API, 替代云语音识别, 私有化语音识别, OpenAI 兼容语音识别, FunASR"><link rel="icon" href="/logo.png">
<meta property="og:title" content="从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收 | FunASR Blog"><meta property="og:description" content="评估自托管转写迁移:映射请求和结果契约,检查鉴权、返回格式、模型质量与运维成本。提供 FunASR 本地 JSON 示例,不承诺厂商 API 或 SDK 直接替换。"><meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<link rel="canonical" href="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="en" href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="zh" href="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="x-default" href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","headline":"自托管替代 Deepgram / AssemblyAI:开源免费的语音识别 API | FunASR 博客","description":"想替代 Deepgram/AssemblyAI 这类按分钟付费的云语音识别 API?FunASR 开源免费、可自托管、音频不出本地、中文领先,自带 OpenAI 兼容 /v1/audio/transcriptions,客户端只改 base_url 即可迁移。","datePublished":"2026-06-23","dateModified":"2026-06-23","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"},"image":"https://www.funasr.com/logo.png"}
{"@context":"https://schema.org","@type":"BlogPosting","headline":"从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收 | FunASR Blog","description":"评估自托管转写迁移:映射请求和结果契约,检查鉴权、返回格式、模型质量与运维成本。提供 FunASR 本地 JSON 示例,不承诺厂商 API 或 SDK 直接替换。","datePublished":"2026-06-23","dateModified":"2026-09-09","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"},"image":"https://www.funasr.com/logo.png"}
</script>
<style>:root{--primary:#2563eb;--primary-dark:#1d4ed8;--bg:#fff;--surface:#f8fafc;--border:#e2e8f0;--text:#0f172a;--text-soft:#475569;--text-muted:#94a3b8;--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
@@ -46,35 +46,54 @@ footer a{color:#94a3b8}
</style></head><body>
<nav class="nav"><div class="container"><a href="/" class="nav-logo">Fun<span>ASR</span></a><div class="nav-links"><a href="/#capabilities">核心能力</a><a href="/#usage">如何使用</a><a href="/#benchmark">性能</a><a href="/#demo">演示</a><a href="https://modelscope.github.io/FunASR/zh/" target="_blank">文档</a><a href="/quickstart.html">快速上手</a><a href="/models.html">模型</a><a href="/ecosystem.html">生态</a><a href="/blog/">技术博客</a><a href="/vs-whisper.html">对比 Whisper</a><a href="/donors.html">功德榜</a></div><a href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html" style="font-size:0.83rem;font-weight:500;color:var(--text-soft)">EN</a><a href="https://github.com/modelscope/FunASR" target="_blank" class="nav-btn">GitHub ★</a></div></nav>
<article><div class="container">
<h1>自托管替代 Deepgram / AssemblyAI:开源、免费、数据不出本地</h1>
<p class="post-meta">2026-06-23 · FunASR 团队</p>
<p>Deepgram、AssemblyAI、Google/Azure Speech 这类云端语音识别 API 按分钟计费,音频还得上传到第三方。如果你想<strong>自己托管、零按量费用、音频不出本地</strong>——尤其中文场景——<a href="https://github.com/modelscope/FunASR" target="_blank">FunASR</a> 是一个直接的开源替代:它自带 <strong>OpenAI 兼容的转写 API</strong>,客户端只改 base_url 即可。</p>
<h2>对比</h2>
<table>
<tr><th></th><th>FunASR(自托管)</th><th>Deepgram / AssemblyAI</th></tr>
<tr><td>价格</td><td><strong>免费(MIT),无按分钟计费</strong></td><td>按分钟付费</td></tr>
<tr><td>部署</td><td>自托管(本地/私有云/边缘)</td><td>云 API(托管)</td></tr>
<tr><td>数据隐私</td><td><strong>音频不出本地</strong></td><td>上传到供应商</td></tr>
<tr><td>中文/粤语</td><td><strong>业界领先</strong></td><td>以英文为主</td></tr>
<tr><td>接口</td><td>OpenAI 兼容(drop-in)</td><td>各自专有 SDK</td></tr>
<tr><td>离线/CPU</td><td>✅(含 <a href="/llama-cpp.html">llama.cpp 单二进制</a>)</td><td>❌ 仅云</td></tr>
</table>
<p>云 API 的优势是免运维、弹性扩缩、英文与增值功能成熟;FunASR 的优势是免费、自托管、隐私、中文。按需求取舍。</p>
<h2>起一个 OpenAI 兼容的本地 STT 服务</h2>
<pre>pip install funasr
funasr-server --model sensevoice --device cuda # 或 --device cpu
# → POST http://localhost:8000/v1/audio/transcriptions</pre>
<p>然后任何 OpenAI 客户端只改 <code>base_url</code> 即可——无需改业务代码:</p>
<pre>from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
text = client.audio.transcriptions.create(model="sensevoice", file=open("audio.wav","rb")).text</pre>
<p>从 Deepgram/AssemblyAI 或 OpenAI Whisper API 迁移:把请求指向你自己的 funasr-server 即可,按量账单归零、数据留在本地。</p>
<h2>选模型</h2>
<p>多语种 + 情感选 SenseVoice;中文 + 时间戳/热词选 Paraformer;中英日及中文方言/口音的旗舰识别选 Fun-ASR-Nano;需要 31 语种选独立的 Fun-ASR-MLT-Nano。详见 <a href="/blog/which-funasr-model.html">选型指南</a>。</p>
<div class="cta"><p><strong>FunASR 全家桶开源、可商用、可自托管。</strong>觉得有用点个 Star 👇</p><p><a class="btn" href="https://github.com/modelscope/FunASR" target="_blank">⭐ Star FunASR</a></p><p style="margin-top:10px;font-size:0.85rem">也欢迎:<a href="https://github.com/QwenAudio/SenseVoice" target="_blank">SenseVoice</a> · <a href="https://github.com/QwenAudio/Fun-ASR" target="_blank">Fun-ASR</a> · <a href="https://github.com/modelscope/FunClip" target="_blank">FunClip</a></p></div>
<h1>从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收清单</h1>
<p class="post-meta">FunASR Blog · 2026-06-23 · 更新于 2026-09-09</p>
<p>评估自托管语音转写时,真正要迁移的是应用契约:上传方式、模型选择、鉴权、结果结构和运行责任。本文以 <strong>FunASR 1.4.15 的包内 <code>funasr-server</code></strong> 为边界,介绍文件转写的接入路径。它不是 Deepgram / AssemblyAI 专有 API 或 SDK 的直接替换,也不提供完整云服务功能对等承诺。</p>
<aside class="limitation-callout" id="security-boundary" data-http-contract="security">
<p><strong>先确定访问边界。</strong><span data-risk="authentication">包内服务默认监听 <code>0.0.0.0</code>,没有内置认证。</span><span data-risk="upload-limit">FunASR 的该入口没有实现应用层总上传大小上限。</span>下方显式使用 <code>--host 127.0.0.1</code>,仅用于客户端与后端位于同一主机网络命名空间的本地检查。</p>
<p data-risk="backend-bypass">对外服务前应在网关配置 TLS、鉴权、上传限制、容量与超时策略,并阻止绕过网关直接访问后端。CORS 不是访问控制;客户端占位 API key 也不是认证。请先阅读<a href="/docs/security.html">服务安全指南</a>。这里说明部署风险,不意味着已经发现某个线上服务暴露。</p>
</aside>
<h2>先列迁移清单,再选择模型</h2>
<table><thead><tr><th>应用环节</th><th>迁移时要验证什么</th></tr></thead><tbody>
<tr><td>请求协议</td><td>把现有 SDK 或 HTTP 调用映射到目标路由、multipart 文件上传及其实际声明的字段。连接地址、请求头和参数都需要检查。</td></tr>
<tr><td>任务方式</td><td>本地示例是一次文件上传转写,不是流式会话、异步任务队列或回调服务。原应用依赖这些流程时,需要单独实现或选择对应部署服务。</td></tr>
<tr><td>结果结构</td><td>明确文本、时间单位、分段、说话人标签以及空结果的映射。说话人分离标签不是实名身份识别;不同模型提供的信息不同。</td></tr>
<tr><td>故障处理</td><td>检查鉴权失败、无效文件、超时、重试与过载行为;客户端超时不等于后台推理已经取消。</td></tr>
<tr><td>运行责任</td><td>安排容量、监控、升级、数据留存及故障恢复。用自己的音频评估质量,不按未经验证的语言或价格排名选型。</td></tr>
</tbody></table>
<p>先参考<a href="/docs/model-selection.html">模型选型指南</a>和<a href="/deploy/">部署矩阵</a>。需要转写与说话人分离时,可评估第三方模型 <a href="/docs/moss-transcribe-diarize.html">MOSS-Transcribe-Diarize</a> 的集成路径;不要把任一后端的能力推定为所有 FunASR 服务都具备。</p>
<h2>最小本地接入:先验证 JSON 文本</h2>
<section data-http-contract="prerequisites">
<p><strong>前提:已经准备并验证过的独立 FunASR 1.4.15 环境。</strong>本段不是从零安装教程。运行下列命令的 Python 和 <code>funasr-server</code> 必须来自同一个已激活环境,且 <code>funasr==1.4.15</code>、PyTorch、音频依赖、FastAPI、Uvicorn、python-multipart、模型权重访问及音频文件均已准备好。平台依赖参见<a href="/docs/installation.html">安装指南</a>;其他版本需要重新核对行为。</p>
<pre><code>python -c 'from importlib.metadata import version; assert version("funasr") == "1.4.15", version("funasr")'
python -m pip check</code></pre>
<p>版本检查和 <code>pip check</code> 只检查包元数据,不能证明 CUDA、音频解码或实际转写可用。下方明确选择 SenseVoice CPU 做接入验证,不是性能建议;不要在同一端口同时启动多个服务。另有<a href="/docs/http-server.html">仓库示例 HTTP 服务指南</a>可作实现对照,但它准备并启动的是 example server,不是本文所要求的包内 1.4.15 环境。</p>
</section>
<pre><code>funasr-server --host 127.0.0.1 --port 8000 --model sensevoice --device cpu</code></pre>
<p>在同一主机另一个终端提交有权处理的 <code>audio.wav</code>。返回文本会写到终端,请勿把私人转写内容放进公开日志。</p>
<pre><code>curl --fail --show-error --silent --max-time 120 \
http://127.0.0.1:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice" \
-F "response_format=json"</code></pre>
<p>预期结构是包含 <code>text</code> 的 JSON 对象,具体内容取决于音频与模型。120 秒是示例客户端超时,不是可处理音频时长或吞吐承诺。使用 OpenAI Python SDK 的基本调用可参考<a href="/blog/self-hosted-openai-whisper-api-alternative.html">本地转写接入指南</a>;这不意味着原厂商 SDK 可以原样使用。</p>
<h2>明确返回格式边界</h2>
<table data-http-contract="formats"><thead><tr><th>response_format</th><th>包内处理器的行为</th></tr></thead><tbody>
<tr data-response-format="json"><td><code>json</code></td><td>返回含 <code>text</code> 的 JSON 对象。</td></tr>
<tr data-response-format="verbose_json"><td><code>verbose_json</code></td><td>返回结构化 JSON,包括 <code>segments</code>、<code>duration</code> 等字段;具体时间与说话人信息依赖模型和实际结果。</td></tr>
<tr data-response-format="text"><td><code>text</code></td><td>返回 JSON 字符串,不是普通纯文本响应。</td></tr>
<tr data-response-format="srt"><td><code>srt</code></td><td>不生成 SRT 字幕,落入 JSON 文本对象分支;HTTP 200 不能证明返回了字幕文件。</td></tr>
<tr data-response-format="vtt"><td><code>vtt</code></td><td>不生成 VTT 字幕,同样落入 JSON 文本对象分支。</td></tr>
</tbody></table>
<p>该路由声明 <code>file</code>、<code>model</code>、<code>language</code>、<code>response_format</code> 和扩展字段 <code>spk</code>;其他字段被请求接受不代表已经应用。</p>
<p data-http-contract="schema-authority">包内服务的字段应通过私有或受限的运维路径访问运行中服务的 <code>/openapi.json</code>,并核对<a href="https://github.com/modelscope/FunASR/blob/dff96b69f1679953ac53c6643c82c74b43c70db2/funasr/bin/_server_app.py">固定版本包内处理器源码</a>。<a href="/docs/service-api.html">示例服务 API schema</a>描述的是另一种实现,不是包内处理器的完整字段说明。包内服务、仓库示例、vLLM 与 llama.cpp 的默认值、参数和响应结构并不通用。</p>
<h2>上线前的三项验收</h2>
<ol>
<li><strong>契约:</strong>在旧、新实现上运行有代表性的受控请求,核对字段、文本解析、时间单位、错误与超时;不要以一次 200 响应作为全部兼容证据。</li>
<li><strong>质量:</strong>用业务录音检查关键实体、多人重叠、分段与低质量音频。参考<a href="/blog/meeting-transcript-acceptance.html">会议转写验收清单</a>,记录模型、硬件和版本。</li>
<li><strong>安全与运维:</strong>验证网关认证和后端不可绕过;容器的 <code>127.0.0.1</code> 指向容器自身,跨容器需设计受限网络。按需要评估计算、存储、带宽、监控和维护成本,另行核对所用模型权重许可证。</li>
</ol>
<p>自托管提供数据处理的控制空间,不自动保证音频从不落盘、日志不含转写或零成本。临时文件、代理、客户端及留存策略都要纳入检查。本文不比较厂商当前价格,也不声称新的准确率或性能测试结果。</p>
<!--related-posts-->
<h2>相关文章</h2>
<ul class="post-list">
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="zh"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>自托管 OpenAI Whisper API 替代方案:FunASR 一行起兼容 /v1/audio/transcriptions 服务 | FunASR</title><meta name="description" content="用 FunASR 起一个 OpenAI 兼容的语音转文字服务:`funasr-server` 暴露 /v1/audio/transcriptions,OpenAI SDK 只改 base_url 就能用。本地、免费、隐私,中文更准,比 Whisper 快。全部代码实测。"><meta name="keywords" content="openai whisper api 替代, 自托管语音识别api, openai 语音转文字 api, /v1/audio/transcriptions, whisper api self hosted, openai 兼容 asr, 本地语音转文字服务, funasr server, 替代 openai whisper api, 私有化部署语音识别">
<link rel="icon" href="/logo.png"><meta property="og:title" content="自托管 OpenAI Whisper API 替代方案:FunASR 一行起兼容 /v1/audio/transcriptions 服务"><meta property="og:description" content="用 FunASR 起一个 OpenAI 兼容的语音转文字服务:`funasr-server` 暴露 /v1/audio/transcriptions,OpenAI SDK 只改 base_url 就能用。本地、免费、隐私,中文更准,比 Whisper 快。全部代码实测。">
<title>自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界 | FunASR Blog</title><meta name="description" content="用 FunASR 部署本地文件转写:显式 loopback 监听、基本 JSON 调用、SDK 接入、格式限制与网关安全。区分部分接口兼容与完整云服务替换。"><meta name="keywords" content="openai whisper api 替代, 自托管语音识别api, openai 语音转文字 api, /v1/audio/transcriptions, whisper api self hosted, openai 兼容 asr, 本地语音转文字服务, funasr server, 替代 openai whisper api, 私有化部署语音识别">
<link rel="icon" href="/logo.png"><meta property="og:title" content="自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界 | FunASR Blog"><meta property="og:description" content="用 FunASR 部署本地文件转写:显式 loopback 监听、基本 JSON 调用、SDK 接入、格式限制与网关安全。区分部分接口兼容与完整云服务替换。">
<meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<link rel="canonical" href="https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="en" href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="zh" href="https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="x-default" href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","headline":"自托管 OpenAI Whisper API 替代方案:FunASR 一行起兼容 /v1/audio/transcriptions 服务","description":"用 FunASR 起一个 OpenAI 兼容的语音转文字服务:`funasr-server` 暴露 /v1/audio/transcriptions,OpenAI SDK 只改 base_url 就能用。本地、免费、隐私,中文更准,比 Whisper 快。全部代码实测。","datePublished":"2026-06-18","dateModified":"2026-06-18","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"},"image":"https://www.funasr.com/logo.png"}
{"@context":"https://schema.org","@type":"BlogPosting","headline":"自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界 | FunASR Blog","description":"用 FunASR 部署本地文件转写:显式 loopback 监听、基本 JSON 调用、SDK 接入、格式限制与网关安全。区分部分接口兼容与完整云服务替换。","datePublished":"2026-06-18","dateModified":"2026-09-09","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"},"image":"https://www.funasr.com/logo.png"}
</script>
<style>:root{--primary:#2563eb;--primary-dark:#1d4ed8;--bg:#fff;--surface:#f8fafc;--border:#e2e8f0;--text:#0f172a;--text-soft:#475569;--text-muted:#94a3b8;--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
@@ -47,79 +47,64 @@ footer a{color:#94a3b8}
</style></head><body>
<nav class="nav"><div class="container"><a href="/" class="nav-logo">Fun<span>ASR</span></a><div class="nav-links"><a href="/#capabilities">核心能力</a><a href="/#usage">如何使用</a><a href="/#benchmark">性能</a><a href="/#demo">演示</a><a href="https://modelscope.github.io/FunASR/zh/" target="_blank">文档</a><a href="/quickstart.html">快速上手</a><a href="/models.html">模型</a><a href="/ecosystem.html">生态</a><a href="/blog/">技术博客</a><a href="/vs-whisper.html">对比 Whisper</a><a href="/donors.html">功德榜</a></div><a href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html" style="font-size:0.83rem;font-weight:600;color:var(--text-soft)">EN</a><a href="/go/github" target="_blank" class="nav-btn">GitHub ★</a></div></nav>
<article><div class="container">
<h1>自托管 OpenAI Whisper API 替代方案:FunASR 一行起兼容 /v1/audio/transcriptions 服务</h1>
<p class="post-meta">FunASR 技术博客 · 2026-06-18 · 教程</p>
<p>OpenAI 的 <code>/v1/audio/transcriptions</code>(Whisper API)按分钟计费、要把音频上传到云端、还有速率限制。如果你只是想把音频转成文字,完全可以在自己机器上起一个<strong>接口完全一致</strong>的服务——已有代码只改一个 <code>base_url</code> 就能切过来。FunASR 自带的 <code>funasr-server</code> 就是干这个的。下面每段代码都实测过。</p>
<h2>一行启动 OpenAI 兼容服务</h2>
<pre><code>pip install -U funasr
funasr-server --model sensevoice --device cuda # 监听 localhost:8000</code></pre>
<p>启动后就有了和 OpenAI 一致的两个端点:<code>POST /v1/audio/transcriptions</code> 和 <code>GET /v1/models</code>。</p>
<h2>用 OpenAI 官方 SDK 直接调(只改 base_url)</h2>
<p>你原来调 OpenAI 的代码,几乎一字不改,只把 <code>base_url</code> 指到本地服务、<code>api_key</code> 随便填:</p>
<h1>自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界</h1>
<p class="post-meta">FunASR 技术博客 · 2026-06-18 · 更新于 2026-09-09</p>
<p>把转写任务放到自己的机器上,能让团队自行选择模型、容量与数据处理方式。本文讨论 <strong>FunASR 1.4.15 的包内 <code>funasr-server</code></strong>,演示 <code>POST /v1/audio/transcriptions</code> 的基本 JSON 调用。它提供部分 OpenAI 兼容能力,不代表完整复刻 OpenAI 服务,也不代表任意 SDK 或应用都无需适配。</p>
<aside class="limitation-callout" id="security-boundary" data-http-contract="security">
<p><strong>先确定访问边界。</strong><span data-risk="authentication">包内服务默认监听 <code>0.0.0.0</code>,没有内置认证。</span><span data-risk="upload-limit">FunASR 的该入口没有实现应用层总上传大小上限。</span>下方显式使用 <code>--host 127.0.0.1</code>,仅用于客户端与后端位于同一主机网络命名空间的本地检查。</p>
<p data-risk="backend-bypass">对外服务前应在网关配置 TLS、鉴权、上传限制、容量与超时策略,并阻止绕过网关直接访问后端。CORS 不是访问控制;客户端占位 API key 也不是认证。请先阅读<a href="/docs/security.html">服务安全指南</a>。这里说明部署风险,不意味着已经发现某个线上服务暴露。</p>
</aside>
<h2>先做一次本地 JSON 转写</h2>
<section data-http-contract="prerequisites">
<p><strong>前提:已经准备并验证过的独立 FunASR 1.4.15 环境。</strong>本段不是从零安装教程。运行下列命令的 Python 和 <code>funasr-server</code> 必须来自同一个已激活环境,且 <code>funasr==1.4.15</code>、PyTorch、音频依赖、FastAPI、Uvicorn、python-multipart、模型权重访问及音频文件均已准备好。平台依赖参见<a href="/docs/installation.html">安装指南</a>;其他版本需要重新核对行为。</p>
<pre><code>python -c 'from importlib.metadata import version; assert version("funasr") == "1.4.15", version("funasr")'
python -m pip check</code></pre>
<p>版本检查和 <code>pip check</code> 只检查包元数据,不能证明 CUDA、音频解码或实际转写可用。下方明确选择 SenseVoice CPU 做接入验证,不是性能建议;不要在同一端口同时启动多个服务。另有<a href="/docs/http-server.html">仓库示例 HTTP 服务指南</a>可作实现对照,但它准备并启动的是 example server,不是本文所要求的包内 1.4.15 环境。</p>
</section>
<pre><code>funasr-server --host 127.0.0.1 --port 8000 --model sensevoice --device cpu</code></pre>
<p>在同一主机的另一个终端,上传你有权处理的 <code>audio.wav</code>。命令会输出转写文本,不要把包含私人内容的输出直接贴入公开日志。</p>
<pre><code>curl --fail --show-error --silent --max-time 120 \
http://127.0.0.1:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice" \
-F "response_format=json"</code></pre>
<p>响应是包含 <code>text</code> 的 JSON 对象;具体文本由录音和模型决定。120 秒只是客户端示例超时,不是任意长度音频的处理保证,也不意味着超时会取消后台推理。</p>
<h2>OpenAI Python SDK:明确模型和返回格式</h2>
<p>在单独安装了 <code>openai</code> 客户端的环境中,可以使用以下基本调用。迁移时仍需检查模型名、参数、响应解析、错误处理和网关认证,不能只检查 URL 是否可连接。</p>
<pre><code>from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
with open("audio.wav", "rb") as f:
r = client.audio.transcriptions.create(model="sensevoice", file=f)
print(r.text)
# -&gt; 欢迎大家来体验达摩院推出的语音识别模型</code></pre>
<p>列出可用模型也和 OpenAI 一样:</p>
<pre><code>print([m.id for m in client.models.list().data])
# -&gt; ['fun-asr-nano', 'sensevoice', 'paraformer']</code></pre>
<h2>或者用 curl / 任意 HTTP 客户端</h2>
<pre><code>curl http://localhost:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice"
# -&gt; {"text": "欢迎大家来体验达摩院推出的语音识别模型"}</code></pre>
<p>因为接口是 OpenAI 兼容的,Node 的 <code>openai</code> 包、LangChain、各种现成 SDK 都能直接连。</p>
<h2>对比:OpenAI Whisper API vs FunASR 自托管</h2>
<table><thead><tr><th></th><th>OpenAI Whisper API</th><th>FunASR 自托管</th></tr></thead><tbody>
<tr><td>费用</td><td>$0.006 / 分钟</td><td>免费(自己的机器)</td></tr>
<tr><td>数据隐私</td><td>音频上传到云端</td><td>不出本地</td></tr>
<tr><td>速率限制</td><td>有</td><td>无</td></tr>
<tr><td>中文准确率</td><td>一般</td><td>更高(<a href="/blog/funasr-vs-whisper-benchmark.html">实测对比</a>)</td></tr>
<tr><td>速度</td><td>—</td><td>SenseVoice 非自回归,远快于 Whisper</td></tr>
<tr><td>接口</td><td>/v1/audio/transcriptions</td><td>完全一致</td></tr>
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="not-needed",
timeout=120.0,
max_retries=0,
)
with open("audio.wav", "rb") as audio:
result = client.audio.transcriptions.create(
model="sensevoice", file=audio, response_format="json"
)
print(result.text)</code></pre>
<h2>包内服务的兼容范围</h2>
<table id="api-contract" data-http-contract="formats"><thead><tr><th>项目</th><th>当前行为与接入要求</th></tr></thead><tbody>
<tr><td>请求</td><td>multipart 文件上传;路由声明 <code>file</code>、<code>model</code>、<code>language</code>、<code>response_format</code> 和 FunASR 扩展 <code>spk</code>。语言和说话人能力仍受所选模型限制。</td></tr>
<tr data-response-format="json"><td><code>json</code></td><td>返回带 <code>text</code> 的 JSON 对象,适合先验证基本文本转写。</td></tr>
<tr data-response-format="verbose_json"><td><code>verbose_json</code></td><td>返回结构化 JSON,包括 <code>segments</code>、<code>duration</code> 等字段;具体时间与说话人信息依赖模型和实际结果。</td></tr>
<tr data-response-format="text"><td><code>text</code></td><td>当前包内处理器返回 JSON 字符串,不是标准的纯文本响应;不要假设云端客户端的解析方式完全相同。</td></tr>
<tr data-response-format="srt"><td><code>srt</code></td><td>不生成 SRT 字幕,落入 JSON 文本对象分支;HTTP 200 不能证明返回了字幕文件。</td></tr>
<tr data-response-format="vtt"><td><code>vtt</code></td><td>不生成 VTT 字幕,同样落入 JSON 文本对象分支。</td></tr>
<tr><td>其他参数与协议</td><td><code>prompt</code>、<code>temperature</code>、<code>timestamp_granularities</code> 不是该路由声明的完整能力。不要把它当成流式 WebSocket、翻译或完整云 API;额外字段未报错也不证明生效。</td></tr>
</tbody></table>
<h2>选哪个模型</h2>
<p data-http-contract="schema-authority">包内服务的字段应通过私有或受限的运维路径访问运行中服务的 <code>/openapi.json</code>,并核对<a href="https://github.com/modelscope/FunASR/blob/dff96b69f1679953ac53c6643c82c74b43c70db2/funasr/bin/_server_app.py">固定版本包内处理器源码</a>。<a href="/docs/service-api.html">示例服务 API schema</a>描述的是另一种实现,不是包内处理器的完整字段说明。包内服务、仓库示例、vLLM 与 llama.cpp 的默认值、参数和响应结构并不通用。</p>
<h2>接入 Open WebUI 或其他应用前</h2>
<ul>
<li><code>sensevoice</code> —— 默认,非自回归,极快,中、粤、英、日、韩五种语言,带情感/事件;日常首选。</li>
<li><code>fun-asr-nano</code> —— LLM 解码,面向中英日及中文方言/口音的旗舰模型(需要 vLLM)。</li>
<li>需要 31 语种 —— 部署独立的 <code>FunAudioLLM/Fun-ASR-MLT-Nano-2512</code> checkpoint。</li>
<li><code>paraformer</code> —— 经典中文生产级,稳。</li>
<li>核对应用实际发送的模型别名、文件类型和 <code>response_format</code>,以及成功、空文本、错误和超时的解析路径。</li>
<li>容器内的 <code>127.0.0.1</code> 指向该容器自身。宿主机 loopback 服务不会因为换成 <code>host.docker.internal</code> 就自动可达;跨容器部署需要受限的私有网络和匹配的网关认证。</li>
<li>Basic、Bearer、OIDC 等认证方式不能互换。生产环境不能照搬本地占位 key;预加载一个模型也不是模型访问白名单。</li>
<li>先用受控录音验证端到端流程,再开放真实流量。参见<a href="https://github.com/modelscope/FunASR/blob/main/examples/openai_api/CLIENTS.md">维护中的客户端接入指南</a>。</li>
</ul>
<p>请求里把 <code>model=</code> 换成对应名字即可,无需重启服务。</p>
<h2>从 OpenAI 迁移要改什么</h2>
<ul>
<li><strong>base_url</strong>:指到你的 <code>funasr-server</code>。</li>
<li><strong>api_key</strong>:本地服务不校验,随便填一个非空值。</li>
<li><strong>其余不变</strong>:<code>client.audio.transcriptions.create(...)</code> 的调用方式一致。</li>
</ul>
<h2>在 Open-WebUI 里把语音输入接到 FunASR</h2>
<p><strong>Open-WebUI</strong>(15 万星的自托管 LLM 界面)的语音转文字(STT)支持任意 OpenAI 兼容端点。既然 <code>funasr-server</code> 本身就是 OpenAI 兼容的,直接指过去即可——<strong>无需插件、无需改 Open-WebUI 代码</strong>,中文识别比内置 Whisper 更准,而且本地、免费、隐私。</p>
<p>① 起 FunASR 服务(同上):</p>
<pre>funasr-server --model sensevoice --device cuda # 监听 localhost:8000</pre>
<p>② Open-WebUI 里:<strong>管理面板 → 设置 → 音频 → 语音转文本</strong>,引擎选 <strong>OpenAI</strong>,填:</p>
<ul>
<li>API Base URL:<code>http://localhost:8000/v1</code>(Open-WebUI 在 Docker、FunASR 在宿主机时用 <code>http://host.docker.internal:8000/v1</code>)</li>
<li>API Key:任意非空(funasr-server 不校验)</li>
<li>STT Model:<code>sensevoice</code>(或 <code>paraformer</code> / <code>fun-asr-nano</code>)</li>
</ul>
<p>用 Docker Compose 部署等价于这几个环境变量:</p>
<pre>AUDIO_STT_ENGINE=openai
AUDIO_STT_OPENAI_API_BASE_URL=http://host.docker.internal:8000/v1
AUDIO_STT_OPENAI_API_KEY=funasr
AUDIO_STT_MODEL=sensevoice</pre>
<p>之后 Open-WebUI 的麦克风语音输入就走 FunASR 了——中文转写更准、数据不出本地。同样的配置适用于任何"支持自定义 OpenAI STT 端点"的工具(LibreChat、AnythingLLM 等)。</p>
<div class="cta"><p>FunASR 是通义实验室开源的工业级语音识别工具包。</p><a class="btn" href="/go/github" target="_blank">在 GitHub 上 Star FunASR ★</a></div>
<h2>比较的是完整部署方案,不只是 API 单价</h2>
<p>自托管需要计算服务器、存储、流量、维护与监控成本。是否留在受控网络,取决于客户端、代理、临时文件、日志和保留策略;不能承诺音频绝不落盘或转写永不泄露。模型权重的许可也需要单独核对,不能由工具包代码许可代替。</p>
<p>根据语言、时延、硬件和说话人需求查看<a href="/docs/model-selection.html">模型选型</a>、<a href="/docs/moss-transcribe-diarize.html">MOSS 转写与说话人分离</a>和<a href="/deploy/">部署矩阵</a>。本文不提供新的准确率或性能排名;验收方法见<a href="/blog/meeting-transcript-acceptance.html">会议录音验收清单</a>。</p>
<!--related-posts-->
<h2>相关文章</h2>
<ul class="post-list">
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Self-Hosted Deepgram / AssemblyAI Alternative: Open-Source Speech-to-Text API | FunASR Blog</title><meta name="description" content="Looking to replace per-minute cloud STT APIs like Deepgram or AssemblyAI? FunASR is open-source, free, self-hostable, keeps audio local, leads on Chinese, and exposes an OpenAI-compatible /v1/audio/transcriptions — migrate by changing base_url."><meta name="keywords" content="deepgram alternative, assemblyai alternative, self-hosted speech to text api, open source speech recognition api, replace cloud stt, on-premise speech recognition, openai compatible stt, FunASR"><link rel="icon" href="/logo.png">
<meta property="og:title" content="Self-Hosted Deepgram / AssemblyAI Alternative: Open-Source Speech-to-Text API | FunASR Blog"><meta property="og:description" content="Looking to replace per-minute cloud STT APIs like Deepgram or AssemblyAI? FunASR is open-source, free, self-hostable, keeps audio local, leads on Chinese, and exposes an OpenAI-compatible /v1/audio/transcriptions — migrate by changing base_url."><meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<title>Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR | FunASR Blog</title><meta name="description" content="Plan a self-hosted transcription migration: map request and response contracts, authentication, formats, model quality and operating costs. Includes a local FunASR JSON recipe, not a drop-in vendor SDK replacement."><meta name="keywords" content="deepgram alternative, assemblyai alternative, self-hosted speech to text api, open source speech recognition api, replace cloud stt, on-premise speech recognition, openai compatible stt, FunASR"><link rel="icon" href="/logo.png">
<meta property="og:title" content="Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR | FunASR Blog"><meta property="og:description" content="Plan a self-hosted transcription migration: map request and response contracts, authentication, formats, model quality and operating costs. Includes a local FunASR JSON recipe, not a drop-in vendor SDK replacement."><meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<link rel="canonical" href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="en" href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="zh" href="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html"><link rel="alternate" hreflang="x-default" href="https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","headline":"Self-Hosted Deepgram / AssemblyAI Alternative: Open-Source Speech-to-Text API | FunASR Blog","description":"Looking to replace per-minute cloud STT APIs like Deepgram or AssemblyAI? FunASR is open-source, free, self-hostable, keeps audio local, leads on Chinese, and exposes an OpenAI-compatible /v1/audio/transcriptions — migrate by changing base_url.","datePublished":"2026-06-23","dateModified":"2026-06-23","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"},"image":"https://www.funasr.com/logo.png"}
{"@context":"https://schema.org","@type":"BlogPosting","headline":"Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR | FunASR Blog","description":"Plan a self-hosted transcription migration: map request and response contracts, authentication, formats, model quality and operating costs. Includes a local FunASR JSON recipe, not a drop-in vendor SDK replacement.","datePublished":"2026-06-23","dateModified":"2026-09-09","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/en/blog/self-hosted-deepgram-assemblyai-alternative.html"},"image":"https://www.funasr.com/logo.png"}
</script>
<style>:root{--primary:#2563eb;--primary-dark:#1d4ed8;--bg:#fff;--surface:#f8fafc;--border:#e2e8f0;--text:#0f172a;--text-soft:#475569;--text-muted:#94a3b8;--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
@@ -46,35 +46,54 @@ footer a{color:#94a3b8}
</style></head><body>
<nav class="nav"><div class="container"><a href="/en/" class="nav-logo">Fun<span>ASR</span></a><div class="nav-links"><a href="/en/#capabilities">Capabilities</a><a href="/en/#usage">How to Use</a><a href="/en/#benchmark">Benchmark</a><a href="/en/#demo">Demo</a><a href="https://modelscope.github.io/FunASR/" target="_blank">Docs</a><a href="/en/quickstart.html">Quickstart</a><a href="/en/models.html">Models</a><a href="/en/ecosystem.html">Ecosystem</a><a href="/en/blog/">Blog</a><a href="/en/vs-whisper.html">vs Whisper</a><a href="/en/donors.html">Donors</a></div><a href="https://www.funasr.com/blog/self-hosted-deepgram-assemblyai-alternative.html" style="font-size:0.83rem;font-weight:500;color:var(--text-soft)">中文</a><a href="https://github.com/modelscope/FunASR" target="_blank" class="nav-btn">GitHub ★</a></div></nav>
<article><div class="container">
<h1>Self-Hosted Alternative to Deepgram &amp; AssemblyAI: Open-Source, Free, Data Stays Local</h1>
<p class="post-meta">2026-06-23 · FunASR Team</p>
<p>Cloud speech-to-text APIs like Deepgram, AssemblyAI and Google/Azure Speech bill per minute and require uploading your audio to a third party. If you want to <strong>self-host, pay no per-minute fees, and keep audio on your own machines</strong> — especially for Chinese — <a href="https://github.com/modelscope/FunASR" target="_blank">FunASR</a> is a direct open-source alternative with a built-in <strong>OpenAI-compatible transcription API</strong>: clients just change the base URL.</p>
<h2>Comparison</h2>
<table>
<tr><th></th><th>FunASR (self-hosted)</th><th>Deepgram / AssemblyAI</th></tr>
<tr><td>Price</td><td><strong>Free (MIT), no per-minute billing</strong></td><td>Pay per minute</td></tr>
<tr><td>Deployment</td><td>Self-hosted (local / private cloud / edge)</td><td>Cloud API (managed)</td></tr>
<tr><td>Data privacy</td><td><strong>Audio never leaves your machine</strong></td><td>Uploaded to vendor</td></tr>
<tr><td>Chinese / Cantonese</td><td><strong>Industry-leading</strong></td><td>English-first</td></tr>
<tr><td>Interface</td><td>OpenAI-compatible (drop-in)</td><td>Proprietary SDKs</td></tr>
<tr><td>Offline / CPU</td><td>✅ (incl. <a href="/en/llama-cpp.html">llama.cpp single binary</a>)</td><td>❌ cloud only</td></tr>
</table>
<p>Cloud APIs win on zero-ops, elastic scaling, and mature English/value-added features; FunASR wins on cost, self-hosting, privacy, and Chinese. Choose by your needs.</p>
<h2>Run an OpenAI-compatible local STT server</h2>
<pre>pip install funasr
funasr-server --model sensevoice --device cuda # or --device cpu
# → POST http://localhost:8000/v1/audio/transcriptions</pre>
<p>Then any OpenAI client just changes <code>base_url</code> — no business-code changes:</p>
<pre>from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
text = client.audio.transcriptions.create(model="sensevoice", file=open("audio.wav","rb")).text</pre>
<p>Migrating from Deepgram/AssemblyAI or the OpenAI Whisper API: point requests at your own funasr-server — usage bills go to zero and audio stays local.</p>
<h2>Which model?</h2>
<p>Use SenseVoice for multilingual + emotion; Paraformer for Chinese + timestamps/hotwords; flagship Fun-ASR-Nano for zh/en/ja plus Chinese dialects/accents; and the separate Fun-ASR-MLT-Nano for 31 languages. See the <a href="/en/blog/which-funasr-model.html">model selection guide</a>.</p>
<div class="cta"><p><strong>FunASR is open-source, commercial-friendly, and self-hostable.</strong> A Star really helps 👇</p><p><a class="btn" href="https://github.com/modelscope/FunASR" target="_blank">⭐ Star FunASR</a></p><p style="margin-top:10px;font-size:0.85rem">Also: <a href="https://github.com/QwenAudio/SenseVoice" target="_blank">SenseVoice</a> · <a href="https://github.com/QwenAudio/Fun-ASR" target="_blank">Fun-ASR</a> · <a href="https://github.com/modelscope/FunClip" target="_blank">FunClip</a></p></div>
<h1>Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR: Integration and Acceptance</h1>
<p class="post-meta">FunASR Blog · 2026-06-23 · Updated 2026-09-09</p>
<p>A self-hosted transcription migration is an application-contract change: upload protocols, model selection, authentication, result structures and operational ownership all matter. This guide covers file transcription with the <strong>packaged <code>funasr-server</code> in FunASR 1.4.15</strong>. It is not a direct replacement for proprietary Deepgram / AssemblyAI APIs or SDKs, nor a promise of complete cloud-service feature parity.</p>
<aside class="limitation-callout" id="security-boundary" data-http-contract="security">
<p><strong>Choose the access boundary first.</strong> <span data-risk="authentication">The packaged service defaults to <code>0.0.0.0</code> and has no built-in authentication.</span> <span data-risk="upload-limit">This FunASR entry point implements no application-level total-upload-size limit.</span> The command below explicitly uses <code>--host 127.0.0.1</code> for a local check where client and backend share the same host network namespace.</p>
<p data-risk="backend-bypass">Before sharing the service, enforce TLS, authentication, upload limits, capacity and timeout policies at the gateway, and block direct backend access that bypasses it. CORS is not access control; a placeholder API key is not authentication. Read the <a href="/en/docs/security.html">service security guide</a>. This describes deployment risks, not evidence that a particular running service is exposed.</p>
</aside>
<h2>List migration requirements before choosing a model</h2>
<table><thead><tr><th>Application layer</th><th>What to verify</th></tr></thead><tbody>
<tr><td>Request protocol</td><td>Map the existing SDK or HTTP call to the target route, multipart file upload and declared fields. Check addresses, headers and parameters.</td></tr>
<tr><td>Task lifecycle</td><td>The local example is a single file-upload transcription, not a streaming session, asynchronous job queue or webhook service. Applications depending on those workflows need separate implementation or an appropriate deployment service.</td></tr>
<tr><td>Result structure</td><td>Map text, time units, segments, speaker labels and empty results explicitly. Diarization labels are not verified real-world speaker identities; available information differs by model.</td></tr>
<tr><td>Failure handling</td><td>Check authentication failures, invalid files, timeouts, retries and overload behavior. A client timeout does not prove that background inference has been cancelled.</td></tr>
<tr><td>Operational ownership</td><td>Plan capacity, monitoring, upgrades, retention and recovery. Evaluate your own recordings instead of choosing from unverified language or price rankings.</td></tr>
</tbody></table>
<p>Start with <a href="/en/docs/model-selection.html">model selection</a> and the <a href="/en/deploy/">deployment matrix</a>. For transcription plus speaker diarization, evaluate the integration path for the third-party <a href="/en/docs/moss-transcribe-diarize.html">MOSS-Transcribe-Diarize</a> model. Do not project one backend's capabilities onto every FunASR service.</p>
<h2>Minimal local integration: verify JSON text first</h2>
<section data-http-contract="prerequisites">
<p><strong>Prerequisite: a separate, already prepared and verified FunASR 1.4.15 environment.</strong> This is not a from-scratch installation tutorial. The Python and <code>funasr-server</code> commands must use the same activated environment, with <code>funasr==1.4.15</code>, PyTorch, audio dependencies, FastAPI, Uvicorn, python-multipart, model-weight access and an audio file ready. Use the <a href="/en/docs/installation.html">installation guide</a> for platform dependencies; other versions require a fresh behavior check.</p>
<pre><code>python -c 'from importlib.metadata import version; assert version("funasr") == "1.4.15", version("funasr")'
python -m pip check</code></pre>
<p>The version assertion and <code>pip check</code> inspect package metadata; they do not prove working CUDA, audio decoding or acoustic inference. The command below explicitly selects SenseVoice CPU for an integration check, not a performance recommendation. Do not start two servers on the same port. The separate <a href="/en/docs/http-server.html">repository example HTTP service guide</a> is an implementation comparison: it prepares and starts the example server, not the packaged 1.4.15 environment required here.</p>
</section>
<pre><code>funasr-server --host 127.0.0.1 --port 8000 --model sensevoice --device cpu</code></pre>
<p>From another terminal on the same host, submit an <code>audio.wav</code> file you are authorized to process. Transcript text is printed to the terminal; keep private results out of public logs.</p>
<pre><code>curl --fail --show-error --silent --max-time 120 \
http://127.0.0.1:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice" \
-F "response_format=json"</code></pre>
<p>The expected structure is a JSON object containing <code>text</code>; its content depends on the audio and model. The 120-second client timeout is an example, not an audio-duration or throughput guarantee. For a basic OpenAI Python SDK call, see the <a href="/en/blog/self-hosted-openai-whisper-api-alternative.html">local transcription integration guide</a>. This does not make the original vendor SDK reusable unchanged.</p>
<h2>Make response-format limits explicit</h2>
<table data-http-contract="formats"><thead><tr><th>response_format</th><th>Packaged handler behavior</th></tr></thead><tbody>
<tr data-response-format="json"><td><code>json</code></td><td>Returns a JSON object containing <code>text</code>.</td></tr>
<tr data-response-format="verbose_json"><td><code>verbose_json</code></td><td>Returns structured JSON with fields including <code>segments</code> and <code>duration</code>. Actual timing and speaker information depend on the model and result.</td></tr>
<tr data-response-format="text"><td><code>text</code></td><td>Returns a JSON string, not a plain-text response.</td></tr>
<tr data-response-format="srt"><td><code>srt</code></td><td>Does not produce SRT subtitles; this value reaches the JSON text-object branch. HTTP 200 does not prove that a subtitle file was returned.</td></tr>
<tr data-response-format="vtt"><td><code>vtt</code></td><td>Does not produce VTT subtitles; this value also reaches the JSON text-object branch.</td></tr>
</tbody></table>
<p>The route declares <code>file</code>, <code>model</code>, <code>language</code>, <code>response_format</code> and the extension <code>spk</code>. An extra field being accepted does not prove it was applied.</p>
<p data-http-contract="schema-authority">For the packaged service, verify fields against its running <code>/openapi.json</code> over a private or restricted operational path and the <a href="https://github.com/modelscope/FunASR/blob/dff96b69f1679953ac53c6643c82c74b43c70db2/funasr/bin/_server_app.py">pinned packaged handler source</a>. The <a href="/en/docs/service-api.html">example-service API schema</a> documents a different implementation, not this packaged handler. Packaged, repository example, vLLM and llama.cpp servers do not share every default, field or response shape.</p>
<h2>Three acceptance gates before rollout</h2>
<ol>
<li><strong>Contract:</strong> run representative controlled requests against old and new implementations. Check fields, text parsing, time units, failures and timeouts; one HTTP 200 is not complete compatibility evidence.</li>
<li><strong>Quality:</strong> use business recordings to check key entities, overlapping speakers, segmentation and difficult audio. Follow the <a href="/en/blog/meeting-transcript-acceptance.html">meeting-transcript acceptance checklist</a> and record model, hardware and version.</li>
<li><strong>Security and operations:</strong> test gateway authentication and prevention of backend bypass. A container's <code>127.0.0.1</code> is the container itself; design a restricted network for cross-container access. Assess compute, storage, bandwidth, monitoring and maintenance costs, and review the selected model-weight license separately.</li>
</ol>
<p>Self-hosting provides control over data processing, not an automatic guarantee of no-disk audio handling, transcript-free logs or zero cost. Include temporary files, proxies, clients and retention policies in the review. This article does not compare current vendor prices or claim new accuracy or performance results.</p>
<!--related-posts-->
<h2>Related posts</h2>
<ul class="post-list">
@@ -1,12 +1,12 @@
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Self-Hosted OpenAI Whisper API Alternative: Drop-in /v1/audio/transcriptions with FunASR | FunASR</title><meta name="description" content="Run an OpenAI-compatible speech-to-text server with FunASR: `funasr-server` exposes /v1/audio/transcriptions, and the OpenAI SDK works by changing only base_url. Local, free, private, stronger on Chinese, faster than Whisper. All code tested."><meta name="keywords" content="openai whisper api alternative, self-hosted speech to text api, openai audio transcriptions api, whisper api self-hosted, /v1/audio/transcriptions, openai compatible asr, replace openai whisper api, local whisper api, private speech to text api, funasr server">
<link rel="icon" href="/logo.png"><meta property="og:title" content="Self-Hosted OpenAI Whisper API Alternative: Drop-in /v1/audio/transcriptions with FunASR"><meta property="og:description" content="Run an OpenAI-compatible speech-to-text server with FunASR: `funasr-server` exposes /v1/audio/transcriptions, and the OpenAI SDK works by changing only base_url. Local, free, private, stronger on Chinese, faster than Whisper. All code tested.">
<title>Self-Hosted OpenAI Whisper API Alternative: Integration and Security | FunASR Blog</title><meta name="description" content="Integrate local FunASR file transcription with explicit loopback binding, JSON requests, SDK checks, response-format limits and gateway security. Partial API support is not complete cloud-service parity."><meta name="keywords" content="openai whisper api alternative, self-hosted speech to text api, openai audio transcriptions api, whisper api self-hosted, /v1/audio/transcriptions, openai compatible asr, replace openai whisper api, local whisper api, private speech to text api, funasr server">
<link rel="icon" href="/logo.png"><meta property="og:title" content="Self-Hosted OpenAI Whisper API Alternative: Integration and Security | FunASR Blog"><meta property="og:description" content="Integrate local FunASR file transcription with explicit loopback binding, JSON requests, SDK checks, response-format limits and gateway security. Partial API support is not complete cloud-service parity.">
<meta property="og:type" content="article"><meta property="og:url" content="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"><meta property="og:image" content="https://www.funasr.com/logo.png">
<link rel="canonical" href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="en" href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="zh" href="https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html"><link rel="alternate" hreflang="x-default" href="https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","headline":"Self-Hosted OpenAI Whisper API Alternative: Drop-in /v1/audio/transcriptions with FunASR","description":"Run an OpenAI-compatible speech-to-text server with FunASR: `funasr-server` exposes /v1/audio/transcriptions, and the OpenAI SDK works by changing only base_url. Local, free, private, stronger on Chinese, faster than Whisper. All code tested.","datePublished":"2026-06-18","dateModified":"2026-06-18","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"},"image":"https://www.funasr.com/logo.png"}
{"@context":"https://schema.org","@type":"BlogPosting","headline":"Self-Hosted OpenAI Whisper API Alternative: Integration and Security | FunASR Blog","description":"Integrate local FunASR file transcription with explicit loopback binding, JSON requests, SDK checks, response-format limits and gateway security. Partial API support is not complete cloud-service parity.","datePublished":"2026-06-18","dateModified":"2026-09-09","author":{"@type":"Organization","name":"FunASR / Tongyi Lab"},"publisher":{"@type":"Organization","name":"FunASR","logo":{"@type":"ImageObject","url":"https://www.funasr.com/logo.png"}},"mainEntityOfPage":{"@type":"WebPage","@id":"https://www.funasr.com/en/blog/self-hosted-openai-whisper-api-alternative.html"},"image":"https://www.funasr.com/logo.png"}
</script>
<style>:root{--primary:#2563eb;--primary-dark:#1d4ed8;--bg:#fff;--surface:#f8fafc;--border:#e2e8f0;--text:#0f172a;--text-soft:#475569;--text-muted:#94a3b8;--radius:12px}
*{margin:0;padding:0;box-sizing:border-box}
@@ -47,79 +47,64 @@ footer a{color:#94a3b8}
</style></head><body>
<nav class="nav"><div class="container"><a href="/" class="nav-logo">Fun<span>ASR</span></a><div class="nav-links"><a href="/en/#capabilities">Capabilities</a><a href="/en/#usage">How to Use</a><a href="/en/#benchmark">Benchmark</a><a href="/en/#demo">Demo</a><a href="https://modelscope.github.io/FunASR/" target="_blank">Docs</a><a href="/en/quickstart.html">Quickstart</a><a href="/en/models.html">Models</a><a href="/en/ecosystem.html">Ecosystem</a><a href="/en/blog/">Blog</a><a href="/en/vs-whisper.html">vs Whisper</a><a href="/en/donors.html">Donors</a></div><a href="https://www.funasr.com/blog/self-hosted-openai-whisper-api-alternative.html" style="font-size:0.83rem;font-weight:600;color:var(--text-soft)">中文</a><a href="/go/github" target="_blank" class="nav-btn">GitHub ★</a></div></nav>
<article><div class="container">
<h1>Self-Hosted OpenAI Whisper API Alternative: Drop-in /v1/audio/transcriptions with FunASR</h1>
<p class="post-meta">FunASR Blog · 2026-06-18 · Tutorial</p>
<p>OpenAI's <code>/v1/audio/transcriptions</code> (the Whisper API) charges per minute, uploads your audio to the cloud, and is rate-limited. If you just need audio turned into text, you can run a server with an <strong>identical interface</strong> on your own machine — existing code switches over by changing a single <code>base_url</code>. FunASR ships exactly that as <code>funasr-server</code>. Every snippet below is tested.</p>
<h2>Start an OpenAI-compatible server in one line</h2>
<pre><code>pip install -U funasr
funasr-server --model sensevoice --device cuda # listens on localhost:8000</code></pre>
<p>You now have the same two endpoints OpenAI exposes: <code>POST /v1/audio/transcriptions</code> and <code>GET /v1/models</code>.</p>
<h2>Call it with the official OpenAI SDK (just change base_url)</h2>
<p>Your existing OpenAI code is almost unchanged — point <code>base_url</code> at the local server and put any non-empty <code>api_key</code>:</p>
<h1>Self-Hosted OpenAI Whisper API Alternative: FunASR Integration and Security</h1>
<p class="post-meta">FunASR Blog · 2026-06-18 · Updated 2026-09-09</p>
<p>Self-hosting lets a team choose its speech models, capacity and data-handling policy. This guide covers the <strong>packaged <code>funasr-server</code> in FunASR 1.4.15</strong> and a basic JSON request to <code>POST /v1/audio/transcriptions</code>. It implements part of the OpenAI transcription interface, not the complete hosted service or every SDK and application contract.</p>
<aside class="limitation-callout" id="security-boundary" data-http-contract="security">
<p><strong>Choose the access boundary first.</strong> <span data-risk="authentication">The packaged service defaults to <code>0.0.0.0</code> and has no built-in authentication.</span> <span data-risk="upload-limit">This FunASR entry point implements no application-level total-upload-size limit.</span> The command below explicitly uses <code>--host 127.0.0.1</code> for a local check where client and backend share the same host network namespace.</p>
<p data-risk="backend-bypass">Before sharing the service, enforce TLS, authentication, upload limits, capacity and timeout policies at the gateway, and block direct backend access that bypasses it. CORS is not access control; a placeholder API key is not authentication. Read the <a href="/en/docs/security.html">service security guide</a>. This describes deployment risks, not evidence that a particular running service is exposed.</p>
</aside>
<h2>Start with one local JSON transcription</h2>
<section data-http-contract="prerequisites">
<p><strong>Prerequisite: a separate, already prepared and verified FunASR 1.4.15 environment.</strong> This is not a from-scratch installation tutorial. The Python and <code>funasr-server</code> commands must use the same activated environment, with <code>funasr==1.4.15</code>, PyTorch, audio dependencies, FastAPI, Uvicorn, python-multipart, model-weight access and an audio file ready. Use the <a href="/en/docs/installation.html">installation guide</a> for platform dependencies; other versions require a fresh behavior check.</p>
<pre><code>python -c 'from importlib.metadata import version; assert version("funasr") == "1.4.15", version("funasr")'
python -m pip check</code></pre>
<p>The version assertion and <code>pip check</code> inspect package metadata; they do not prove working CUDA, audio decoding or acoustic inference. The command below explicitly selects SenseVoice CPU for an integration check, not a performance recommendation. Do not start two servers on the same port. The separate <a href="/en/docs/http-server.html">repository example HTTP service guide</a> is an implementation comparison: it prepares and starts the example server, not the packaged 1.4.15 environment required here.</p>
</section>
<pre><code>funasr-server --host 127.0.0.1 --port 8000 --model sensevoice --device cpu</code></pre>
<p>From another terminal on the same host, upload an <code>audio.wav</code> file you are authorized to process. The command prints transcript content; keep private audio results out of public logs.</p>
<pre><code>curl --fail --show-error --silent --max-time 120 \
http://127.0.0.1:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice" \
-F "response_format=json"</code></pre>
<p>The response is a JSON object containing <code>text</code>. Its content depends on the recording and model. The 120-second client timeout is an example, not a duration guarantee or proof that timed-out inference is cancelled.</p>
<h2>OpenAI Python SDK: specify the model and format</h2>
<p>The following basic call uses an environment with the separate <code>openai</code> client installed. A migration must still check model names, fields, response parsing, errors and gateway authentication, not just URL reachability.</p>
<pre><code>from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
with open("audio.wav", "rb") as f:
r = client.audio.transcriptions.create(model="sensevoice", file=f)
print(r.text)
# -&gt; 欢迎大家来体验达摩院推出的语音识别模型 (the Chinese sample above)</code></pre>
<p>Listing models works the same way:</p>
<pre><code>print([m.id for m in client.models.list().data])
# -&gt; ['fun-asr-nano', 'sensevoice', 'paraformer']</code></pre>
<h2>Or use curl / any HTTP client</h2>
<pre><code>curl http://localhost:8000/v1/audio/transcriptions \
-F "[email protected]" \
-F "model=sensevoice"
# -&gt; {"text": "..."}</code></pre>
<p>Because the API is OpenAI-compatible, the Node <code>openai</code> package, LangChain, and most existing SDKs connect to it unchanged.</p>
<h2>OpenAI Whisper API vs self-hosted FunASR</h2>
<table><thead><tr><th></th><th>OpenAI Whisper API</th><th>FunASR self-hosted</th></tr></thead><tbody>
<tr><td>Cost</td><td>$0.006 / min</td><td>Free (your hardware)</td></tr>
<tr><td>Privacy</td><td>Audio leaves your machine</td><td>Stays local</td></tr>
<tr><td>Rate limits</td><td>Yes</td><td>None</td></tr>
<tr><td>Chinese accuracy</td><td>Mediocre</td><td>Higher (<a href="/en/blog/funasr-vs-whisper-benchmark.html">benchmark</a>)</td></tr>
<tr><td>Speed</td><td>—</td><td>SenseVoice is non-autoregressive, far faster than Whisper</td></tr>
<tr><td>Interface</td><td>/v1/audio/transcriptions</td><td>Identical</td></tr>
client = OpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="not-needed",
timeout=120.0,
max_retries=0,
)
with open("audio.wav", "rb") as audio:
result = client.audio.transcriptions.create(
model="sensevoice", file=audio, response_format="json"
)
print(result.text)</code></pre>
<h2>Compatibility of the packaged handler</h2>
<table id="api-contract" data-http-contract="formats"><thead><tr><th>Item</th><th>Current behavior and integration requirement</th></tr></thead><tbody>
<tr><td>Request</td><td>Multipart upload. The route declares <code>file</code>, <code>model</code>, <code>language</code>, <code>response_format</code> and the FunASR extension <code>spk</code>. Language and speaker capabilities depend on the selected model.</td></tr>
<tr data-response-format="json"><td><code>json</code></td><td>A JSON object with <code>text</code>, suitable for a first basic transcription check.</td></tr>
<tr data-response-format="verbose_json"><td><code>verbose_json</code></td><td>Returns structured JSON with fields including <code>segments</code> and <code>duration</code>. Actual timing and speaker information depend on the model and result.</td></tr>
<tr data-response-format="text"><td><code>text</code></td><td>The packaged handler returns a JSON string, not a standard plain-text response. Do not assume identical cloud-client parsing.</td></tr>
<tr data-response-format="srt"><td><code>srt</code></td><td>Does not produce SRT subtitles; this value reaches the JSON text-object branch. HTTP 200 does not prove that a subtitle file was returned.</td></tr>
<tr data-response-format="vtt"><td><code>vtt</code></td><td>Does not produce VTT subtitles; this value also reaches the JSON text-object branch.</td></tr>
<tr><td>Other fields and protocols</td><td><code>prompt</code>, <code>temperature</code> and <code>timestamp_granularities</code> are not declared capabilities of this route. It is not a streaming WebSocket, translation or complete cloud API. An extra field causing no error does not prove it was applied.</td></tr>
</tbody></table>
<h2>Which model to pick</h2>
<p data-http-contract="schema-authority">For the packaged service, verify fields against its running <code>/openapi.json</code> over a private or restricted operational path and the <a href="https://github.com/modelscope/FunASR/blob/dff96b69f1679953ac53c6643c82c74b43c70db2/funasr/bin/_server_app.py">pinned packaged handler source</a>. The <a href="/en/docs/service-api.html">example-service API schema</a> documents a different implementation, not this packaged handler. Packaged, repository example, vLLM and llama.cpp servers do not share every default, field or response shape.</p>
<h2>Before connecting Open WebUI or another application</h2>
<ul>
<li><code>sensevoice</code> — default; non-autoregressive, very fast, Mandarin, Cantonese, English, Japanese, and Korean, with emotion/events. Best everyday choice.</li>
<li><code>fun-asr-nano</code> — flagship LLM decoder for zh/en/ja plus Chinese dialects/accents (needs vLLM).</li>
<li>For 31 languages, deploy the separate <code>FunAudioLLM/Fun-ASR-MLT-Nano-2512</code> checkpoint.</li>
<li><code>paraformer</code> — classic, production-grade Chinese.</li>
<li>Inspect the application's actual model alias, file type and <code>response_format</code>, plus how it handles success, empty text, errors and timeouts.</li>
<li>A container's <code>127.0.0.1</code> is the container itself. A host loopback listener does not become reachable merely by changing a URL to <code>host.docker.internal</code>. Cross-container setups need a restricted private network and matching gateway authentication.</li>
<li>Basic, Bearer and OIDC credentials are not interchangeable. Do not reuse the local placeholder as a production credential. Preloading one model is not a model-admission policy.</li>
<li>Verify the entire path with controlled audio before admitting real traffic. Follow the <a href="https://github.com/modelscope/FunASR/blob/main/examples/openai_api/CLIENTS.md">maintained client recipes</a>.</li>
</ul>
<p>Just set <code>model=</code> in the request — no server restart needed.</p>
<h2>What changes when migrating from OpenAI</h2>
<ul>
<li><strong>base_url</strong>: point it at your <code>funasr-server</code>.</li>
<li><strong>api_key</strong>: not validated locally — any non-empty string works.</li>
<li><strong>Everything else</strong>: the <code>client.audio.transcriptions.create(...)</code> call is the same.</li>
</ul>
<h2>Use FunASR as the speech-to-text in Open-WebUI</h2>
<p><strong>Open-WebUI</strong> (the 150k-star self-hosted LLM interface) lets its speech-to-text (STT) point at any OpenAI-compatible endpoint. Since <code>funasr-server</code> <em>is</em> OpenAI-compatible, you just point it there — <strong>no plugin, no Open-WebUI code changes</strong> — and get more accurate Chinese transcription than the built-in Whisper, fully local and free.</p>
<p>1. Start FunASR (as above):</p>
<pre>funasr-server --model sensevoice --device cuda # listens on localhost:8000</pre>
<p>2. In Open-WebUI: <strong>Admin Panel &rarr; Settings &rarr; Audio &rarr; Speech-to-Text</strong>, set Engine to <strong>OpenAI</strong> and fill in:</p>
<ul>
<li>API Base URL: <code>http://localhost:8000/v1</code> (if Open-WebUI runs in Docker and FunASR on the host, use <code>http://host.docker.internal:8000/v1</code>)</li>
<li>API Key: any non-empty string (funasr-server does not validate it)</li>
<li>STT Model: <code>sensevoice</code> (or <code>paraformer</code> / <code>fun-asr-nano</code>)</li>
</ul>
<p>With Docker Compose this is the same as a few environment variables:</p>
<pre>AUDIO_STT_ENGINE=openai
AUDIO_STT_OPENAI_API_BASE_URL=http://host.docker.internal:8000/v1
AUDIO_STT_OPENAI_API_KEY=funasr
AUDIO_STT_MODEL=sensevoice</pre>
<p>Now Open-WebUI's microphone / voice input runs through FunASR — better Chinese accuracy, data never leaves your machine. The same config works for any tool that supports a custom OpenAI STT endpoint (LibreChat, AnythingLLM, etc.).</p>
<div class="cta"><p>FunASR is Tongyi Lab's open-source, industrial-grade speech recognition toolkit.</p><a class="btn" href="/go/github" target="_blank">Star FunASR on GitHub ★</a></div>
<h2>Compare complete deployments, not just API prices</h2>
<p>Self-hosting has compute, storage, bandwidth, maintenance and monitoring costs. Data control depends on the client, proxy, temporary files, logs and retention policy; neither no-disk handling nor leak-free transcripts are automatic. Review model-weight licenses separately from the toolkit's code license.</p>
<p>Choose by language, latency, hardware and speaker requirements using <a href="/en/docs/model-selection.html">model selection</a>, <a href="/en/docs/moss-transcribe-diarize.html">MOSS transcription and diarization</a> and the <a href="/en/deploy/">deployment matrix</a>. This article supplies no new accuracy or performance ranking. For acceptance criteria, see the <a href="/en/blog/meeting-transcript-acceptance.html">meeting-audio checklist</a>.</p>
<!--related-posts-->
<h2>Related posts</h2>
<ul class="post-list">
@@ -0,0 +1,69 @@
import { expect, test } from '@playwright/test';
const slugs = [
'self-hosted-openai-whisper-api-alternative.html',
'self-hosted-deepgram-assemblyai-alternative.html',
];
for (const prefix of ['', 'en/']) {
for (const slug of slugs) {
for (const width of [390, 1440]) {
test(`HTTP blog contract ${prefix}${slug} at ${width}px`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 900 });
await page.goto(`/${prefix}blog/`);
const route = `/${prefix}blog/${slug}`;
await page.locator(`a.post-card[href="${route}"]`).click();
await expect(page).toHaveURL(new RegExp(slug.replaceAll('.', '\\.')));
const warning = page.locator('[data-http-contract="security"]');
await expect(warning).toBeVisible();
const layout = await page.evaluate(() => {
const article = document.querySelector('article')!;
const heading = article.querySelector('h1')!.getBoundingClientRect();
const nav = document.querySelector('nav')!.getBoundingClientRect();
const warning = article.querySelector('[data-http-contract="security"]')!.getBoundingClientRect();
const firstCommand = article.querySelector('pre')!.getBoundingClientRect();
return {
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
headingTop: heading.top,
navBottom: nav.bottom,
warningBottom: warning.bottom,
commandTop: firstCommand.top,
};
});
expect(layout.overflow).toBeLessThanOrEqual(1);
expect(layout.headingTop).toBeGreaterThanOrEqual(layout.navBottom);
expect(layout.warningBottom).toBeLessThanOrEqual(layout.commandTop);
await page.screenshot({ path: testInfo.outputPath('article-top.png') });
const formats = page.locator('[data-http-contract="formats"]');
await formats.scrollIntoViewIfNeeded();
for (const format of ['json', 'verbose_json', 'text', 'srt', 'vtt']) {
await expect(formats.locator(`[data-response-format="${format}"]`)).toBeVisible();
}
const scroll = await formats.evaluate((table) => {
const wrapper = table.closest('.table-wrap')!;
const box = wrapper.getBoundingClientRect();
wrapper.scrollLeft = wrapper.scrollWidth;
return {
left: box.left,
right: box.right,
width: wrapper.clientWidth,
content: wrapper.scrollWidth,
scrolled: wrapper.scrollLeft,
};
});
expect(scroll.left).toBeGreaterThanOrEqual(0);
expect(scroll.right).toBeLessThanOrEqual(width);
if (width === 390) {
expect(scroll.content).toBeGreaterThan(scroll.width);
expect(scroll.scrolled).toBeGreaterThan(0);
expect(scroll.scrolled + scroll.width).toBeGreaterThanOrEqual(scroll.content - 1);
}
await page.screenshot({ path: testInfo.outputPath('format-boundary.png') });
await warning.locator(`a[href="/${prefix}docs/security.html"]`).click();
await expect(page.locator('[data-source-link]')).toHaveAttribute(
'href', new RegExp(`/examples/openai_api/SECURITY${prefix ? '' : '_zh'}\\.md$`),
);
});
}
}
}
@@ -0,0 +1,351 @@
"""Keep legacy HTTP articles consistent with the maintained packaged API."""
import ast
import json
import re
import shlex
import sys
from pathlib import Path
from urllib.parse import unquote, urlsplit
import pytest
from bs4 import BeautifulSoup
SITE = Path(__file__).resolve().parents[1]
REPO = SITE.parents[1]
sys.path.insert(0, str(SITE))
from build import build
SLUGS = (
"self-hosted-openai-whisper-api-alternative.html",
"self-hosted-deepgram-assemblyai-alternative.html",
)
PAGES = [(prefix, slug) for prefix in ("", "en/") for slug in SLUGS]
NEGATIVE = r"\b(?:no|not|without|unsupported|unimplemented)\b|不|未|没有|缺少"
FORBIDDEN = (
r"接口完全一致",
r"\bidentical\s+interface\b",
r"\binterface\s+(?:is\s+)?identical\b",
r"(?:只改|只需(?:修改|更改)|仅改|仅需(?:修改|更改)).{0,20}base[_ ]url.{0,15}(?:即可|就能|便可)",
r"(?:just|only)\s+(?:change|changing)\s+(?:a\s+single\s+|the\s+)?base[_ ]url",
r"changing\s+(?:only\s+|a\s+single\s+)base[_ ]url",
r"\beverything\s+else\s*:\s*the\s+client\.audio\.transcriptions\.create.*?same",
r"其余不变.*?client\.audio\.transcriptions\.create",
r"(?:usage\s+bills?\s+go(?:es)?\s+to\s+zero|按量账单归零)",
)
@pytest.fixture(scope="module", params=("source", "built"))
def site_tree(request, tmp_path_factory):
if request.param == "source":
return SITE / "legacy"
output = tmp_path_factory.mktemp("http-blog-contracts")
build(output)
return output
def read_page(root, prefix, slug):
return BeautifulSoup((root / prefix / "blog" / slug).read_text(), "html.parser")
def visible_text(node):
assert node is not None, "Missing visible contract explanation"
for ancestor in [node, *node.parents]:
assert not ancestor.has_attr("hidden")
assert ancestor.get("aria-hidden") != "true"
assert not re.search(r"display\s*:\s*none|visibility\s*:\s*hidden", ancestor.get("style", ""))
text = " ".join(node.get_text(" ", strip=True).split())
assert text, "A data attribute alone is not a contract explanation"
return text
def startup_commands(soup):
commands = []
for pre in soup.select("article pre"):
for line in pre.get_text().replace("\\\n", " ").splitlines():
lexer = shlex.shlex(line, posix=True, punctuation_chars=";&|()")
lexer.whitespace_split = True
tokens = list(lexer)
for index, token in enumerate(tokens):
if Path(token).name != "funasr-server":
continue
command = []
for argument in tokens[index:]:
if argument and set(argument) <= set(";&|()"):
break
command.append(argument)
commands.append((pre, command))
assert commands, "Retain at least one usable local startup recipe"
return commands
def assert_loopback(command):
hosts = []
for index, argument in enumerate(command):
if argument == "--host":
assert index + 1 < len(command), command
hosts.append(command[index + 1])
elif argument.startswith("--host="):
hosts.append(argument.partition("=")[2])
assert hosts == ["127.0.0.1"], command
def prose_and_metadata(soup):
chunks = [soup.title.get_text() if soup.title else ""]
chunks.extend(node.get("content", "") for node in soup.select(
'meta[name="description"], meta[property="og:description"], meta[property="og:title"]'
))
for script in soup.select('script[type="application/ld+json"]'):
data = json.loads(script.get_text())
records = data if isinstance(data, list) else [data]
for record in records:
chunks.extend(str(record.get(key, "")) for key in ("headline", "description"))
chunks.append(soup.select_one("article").get_text(" ", strip=True))
return [" ".join(chunk.split()) for chunk in chunks]
def assert_no_blanket_claims(text):
for pattern in FORBIDDEN:
for match in re.finditer(pattern, text, re.I):
# Explicit rejection of an old claim is not a positive promise.
before = text[max(0, match.start() - 35):match.start()]
if re.search(r"(?:not|never)\s+(?:an?\s+)?$|(?:并非|不是|不能|不意味着)\s*$", before, re.I):
continue
pytest.fail(f"Unqualified migration claim: {match.group(0)}")
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_all_local_startup_commands_explicitly_bind_loopback(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
for _, command in startup_commands(soup):
assert_loopback(command)
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_security_boundary_precedes_first_startup(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
warning = soup.select_one('article [data-http-contract="security"]')
visible_text(warning)
first_pre = startup_commands(soup)[0][0]
order = {id(node): index for index, node in enumerate(soup.find_all(True))}
for risk, concept in (
("authentication", r"auth|认证|鉴权"),
("upload-limit", r"upload|request.body|上传|请求体"),
("backend-bypass", r"bypass|绕过"),
):
item = warning.select_one(f'[data-risk="{risk}"]')
text = visible_text(item)
assert re.search(concept, text, re.I), (risk, text)
assert order[id(item)] < order[id(first_pre)], "Security explanation must precede startup"
if risk != "backend-bypass":
assert re.search(NEGATIVE, text, re.I), (risk, text)
assert re.search(r"built.in|implement|FunASR|内置|实现", text, re.I), text
if risk == "upload-limit":
assert re.search(r"limit|cap|上限|限制", text, re.I), text
else:
assert re.search(r"backend|back.end|后端", text, re.I), text
assert re.search(r"gateway|proxy|网关|代理", text, re.I), text
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_links_reach_maintained_same_language_contracts(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
links = soup.select("article a[href]")
for route in (f"/{prefix}docs/security.html", f"/{prefix}docs/http-server.html"):
matching = [link for link in links if urlsplit(link["href"]).path == route
and not urlsplit(link["href"]).netloc]
assert matching, route
for link in matching:
visible_text(link)
if site_tree != SITE / "legacy":
target = BeautifulSoup((site_tree / route.lstrip("/")).read_text(), "html.parser")
fragment = unquote(urlsplit(link["href"]).fragment)
if fragment:
assert target.find(id=fragment), (route, fragment)
source = target.select_one("[data-source-link]")
expected = "SECURITY" if "security.html" in route else "README"
suffix = "" if prefix else "_zh"
assert source["href"].endswith(f"/examples/openai_api/{expected}{suffix}.md")
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_migration_claims_are_bounded_in_article_metadata_and_index(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
for text in prose_and_metadata(soup):
assert_no_blanket_claims(text)
index = read_page(site_tree, prefix, "index.html")
cards = index.select(f'a.post-card[href="/{prefix}blog/{slug}"]')
assert len(cards) == 1
assert_no_blanket_claims(visible_text(cards[0]))
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_visible_response_format_limits_match_packaged_handler(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
section = soup.select_one('article [data-http-contract="formats"]')
visible_text(section)
for name in ("json", "verbose_json", "text", "srt", "vtt"):
item = section.select_one(f'[data-response-format="{name}"]')
text = visible_text(item)
assert re.search(rf"\b{re.escape(name)}\b", text, re.I), text
if name in ("srt", "vtt"):
assert re.search(r"unsupported|not\s+(?:currently\s+)?supported|does\s+not\s+(?:produce|support)|不支持|未支持|不提供|不生成", text, re.I), text
else:
assert "json" in text.lower(), text
if name == "text":
assert re.search(r"string|quoted|字符串|引号", text, re.I), text
elif name == "verbose_json":
assert "segments" in text and "duration" in text, text
else:
assert "text" in text, text
def shell_argvs(section):
for pre in section.select("pre"):
for line in pre.get_text().replace("\\\n", " ").splitlines():
lexer = shlex.shlex(line, posix=True, punctuation_chars=";&|()")
lexer.whitespace_split = True
argv = []
for token in lexer:
if token and set(token) <= set(";&|()"):
if argv:
yield argv
argv = []
else:
argv.append(token)
if argv:
yield argv
def asserts_installed_funasr_version(source, release):
tree = ast.parse(source)
imports = {}
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
imports[name.asname or name.name.split(".")[0]] = (
name.name if name.asname else name.name.split(".")[0]
)
elif isinstance(node, ast.ImportFrom) and node.module:
for name in node.names:
imports[name.asname or name.name] = f"{node.module}.{name.name}"
def qualified(node):
if isinstance(node, ast.Name):
return imports.get(node.id, node.id)
if isinstance(node, ast.Attribute):
return f"{qualified(node.value)}.{node.attr}"
return ""
for node in ast.walk(tree):
if not isinstance(node, ast.Assert) or not isinstance(node.test, ast.Compare):
continue
comparison = node.test
if len(comparison.ops) != 1 or not isinstance(comparison.ops[0], ast.Eq):
continue
pair = (comparison.left, comparison.comparators[0])
for call, expected in (pair, pair[::-1]):
if (isinstance(call, ast.Call)
and qualified(call.func) == "importlib.metadata.version"
and len(call.args) == 1
and isinstance(call.args[0], ast.Constant)
and call.args[0].value == "funasr"
and isinstance(expected, ast.Constant)
and expected.value == release):
return True
return False
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_packaged_prerequisites_are_verified_before_startup(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
section = soup.select_one('article [data-http-contract="prerequisites"]')
text = visible_text(section)
for concept in (
r"separate|isolated|dedicated|独立|隔离",
r"prepared|ready|准备好|已准备|已配置",
r"torch|PyTorch",
r"audio|音频",
r"model|checkpoint|模型|权重",
):
assert re.search(concept, text, re.I), (concept, text)
assert any(re.search(r"inference|transcription|推理|转写", sentence, re.I)
and re.search(NEGATIVE, sentence, re.I)
for sentence in re.split(r"[.!?。!?]\s*", text)), (
"Explain that package checks do not prove inference"
)
assert re.search(r"access|available|可用|访问|获取", text, re.I), text
order = {id(node): index for index, node in enumerate(soup.find_all(True))}
first_start = startup_commands(soup)[0][0]
assert order[id(section)] < order[id(first_start)]
for pre in section.select("pre"):
assert order[id(pre)] < order[id(first_start)]
release = "1.4.15"
commands = list(shell_argvs(section))
python_commands = [args for args in commands if re.fullmatch(
r"python(?:\d+(?:\.\d+)*)?", Path(args[0]).name
)]
assert any(args[1:] == ["-m", "pip", "check"] for args in python_commands)
checks = [args[2] for args in python_commands if len(args) == 3 and args[1] == "-c"]
assert any(asserts_installed_funasr_version(code, release) for code in checks), (
"Check installed distribution metadata against the documented release, not a print or literal"
)
assert release in text, text
@pytest.mark.parametrize("prefix,slug", PAGES)
def test_example_links_do_not_own_packaged_installation_or_schema(site_tree, prefix, slug):
soup = read_page(site_tree, prefix, slug)
for basename in ("http-server", "service-api"):
route = f"/{prefix}docs/{basename}.html"
links = [link for link in soup.select("article a[href]")
if urlsplit(link["href"]).path == route
and not urlsplit(link["href"]).netloc]
assert links, route
for link in links:
assert re.search(r"example|示例", visible_text(link), re.I), (
"Label the linked implementation rather than implying packaged ownership", route
)
authority = soup.select_one('article [data-http-contract="schema-authority"]')
text = visible_text(authority)
assert "/openapi.json" in text
assert re.search(r"private|restricted|internal|私有|受限|内网|内部", text, re.I), text
assert re.search(r"packaged|包内", text, re.I), text
sources = [link for link in authority.select("a[href]") if re.fullmatch(
r"https://github\.com/modelscope/FunASR/blob/[0-9a-f]{40}/funasr/bin/_server_app\.py",
link["href"].split("#", 1)[0],
)]
assert sources, "Keep an immutable packaged-handler source next to the live-schema authority"
for link in sources:
visible_text(link)
def test_packaged_format_implementation_still_matches_documented_boundary():
tree = ast.parse((REPO / "funasr/bin/_server_app.py").read_text())
handler = next(node for node in ast.walk(tree)
if isinstance(node, ast.AsyncFunctionDef) and node.name == "transcribe")
comparisons = [node for node in ast.walk(handler) if isinstance(node, ast.Compare)
and isinstance(node.left, ast.Name) and node.left.id == "response_format"]
assert {node.comparators[0].value for node in comparisons} == {"verbose_json", "text"}
text_branch = next(node for node in ast.walk(handler) if isinstance(node, ast.If)
and isinstance(node.test, ast.Compare)
and isinstance(node.test.left, ast.Name)
and node.test.left.id == "response_format"
and node.test.comparators[0].value == "text")
returned = text_branch.body[0].value
assert isinstance(returned, ast.Call) and returned.func.id == "JSONResponse"
assert ast.dump(returned.args[0]) == ast.dump(ast.parse('result["text"]', mode="eval").body)
@pytest.mark.parametrize("command", [
"funasr-server --model sensevoice",
"funasr-server --host 0.0.0.0",
"funasr-server --host 127.0.0.1; funasr-server --model sensevoice",
"funasr-server --host 127.0.0.1 --host=0.0.0.0",
])
def test_command_parser_does_not_hide_unsafe_later_startups(command):
soup = BeautifulSoup(f"<article><pre>{command}</pre></article>", "html.parser")
with pytest.raises(AssertionError):
for _, args in startup_commands(soup):
assert_loopback(args)