资讯动态

Whisper语音识别服务接入:OpenAI兼容端点与Docker私有化部署

发布时间:2026/9/16 13:21:04 来源:尧图企业网站定制
简介将开源Whisper语音识别模型封装为OpenAI ChatGPT兼容接口的轻量资源包面向需要快速对接语音转写能力的开发者与运维人员。资源以Docker镜像方式交付同时支持GPU加速与CPU模式自由切换可满足离线或低成本部署需求通过标准接口即可与现有ChatGPT应用生态无缝衔接。包内共14个文件以Python脚本、Dockerfile、XML配置、JSON参数及依赖清单为主整体仅13KB紧凑而不臃肿。使用者可对照主程序与语音处理脚本梳理接口封装思路再结合Dockerfile和依赖清单完成一键构建部署免去手动配置环境的烦琐流程接口参数与模型选项可在JSON配置中灵活调整便于适配不同业务场景。目前已有467人学习浏览适合有一定Docker基础、希望快速搭建Whisper推理服务的工程师参考使用。1. Whisper 语音识别服务接入难在哪,OpenAI 兼容端点怎么解决内部工具里 Whisper 跑起来的成本其实不高,真正麻烦的是怎么把它交给别人用。自己写一个/asr接口,会议室记录脚本、测试平台的自动化用例、字幕处理工具全都得跟着适配;而给 Whisper 包一层与 OpenAI 官方语音转写端点一致的 REST 接口后,问题就简单了:客户端不用改任何逻辑,只改base_url指向本地服务,原来怎么写 OpenAI SDK 调用,现在就怎么写。这个项目做的正是这件事,FastAPI 负责 HTTP 层,faster-whisper 做底层推理,再通过 Docker 一条命令把 CPU 或 GPU 模式的服务拉起来。适合手里已有 OpenAI 系客户端、想切私有化语音转写的团队,也适合要接字幕工具、做批量音频转文本的个人开发者。2. OpenAI 音频接口的兼容层设计:请求契约与项目结构2.1 transcriptions 端点与 ChatGPT 对话接口的关系严格讲,这个项目兼容的是 OpenAI 音频服务里的/v1/audio/transcriptions端点,而不是/v1/chat/completions。两者共用同一套 OpenAI Python SDK,所以客户端把base_url从官方地址切到本地地址后,原本能调 ChatGPT 的代码也可以直接调本地 Whisper,这就是项目名里「ChatGPT 兼容接口」的由来。OpenAI 的 audio 服务有三个端点:/v1/audio/transcriptions做语音转文字,/v1/audio/translations把非英语音频翻译成英文文本,/v1/audio/speech做文字转语音。本项目实现的是前者。与 chat 接口最大的差异在请求体:transcriptions走的是multipart/form-data,不是 JSON,文件字段和表单字段混在一起提交。这意味着基于 FastAPI 实现时,必须安装python-multipart,否则框架解析不了这种格式,请求会直接打到 422。官方请求参数如下,兼容层至少要原样收下这些字段:字段类型必填说明filefile是音频文件,官方限制 25MBmodelstring是官方为whisper-1,本地服务里它只是占位languagestring否ISO-639-1 语言码,如zh、en,不传则自动检测response_formatstring否json、verbose_json、srt、vtt、texttemperaturenumber否采样温度,默认 0promptstring否提示词,可引导专用名词的拼写model参数在本地实现里不参与实际选型,真正的模型配置在options.json里。但客户端会坚持传这个字段,所以路由签名里必须声明它并接收。temperature对 Whisper 的解码影响不大,保留它主要是为了兼容 SDK 的默认行为,避免客户端传了就报错。2.2 whisperAPI 项目结构与一次请求的流转解压 whisperAPI.zip 后,真正要关心的是下面这几个文件:whisperAPI/ ├── main.py # FastAPI 应用,路由、参数校验、响应包装 ├── whisper_script.py # whisper 模型加载、音频解码、推理 ├── options.json # 模型类型、设备、量化方式等配置 ├── requirements.txt # Python 依赖 ├── Dockerfile # 容器镜像构建 └── .dockerignore # 排除 .idea、__pycache__ 等杂项.idea目录和workspace.xml、modules.xml是 PyCharm 工程配置,构建镜像时必须排除,不然会带进一堆本机路径信息,后续维护看着也乱。一次完整请求的流转链路是:客户端 multipart POST 到/v1/audio/transcriptions→ Uvicorn 把请求交给main.py→ 参数校验后音频以字节流读入内存,落成临时文件 → 调用whisper_script.py的transcribe函数 → 模型推理返回结构化结果 → 按response_format组织 JSON 响应回给客户端。模型实例在这个链路里是单例。第一次请求时加载到内存,后续所有请求复用同一份模型权重,不会每个请求都重新读一遍模型文件。代价是首次请求会比较慢,tiny 模型都要几秒,large-v3 可能十几秒,生产环境一般要加启动预热,这个后面会展开。2.3 options.json 参数与模型选型options.json是这份资源里唯一需要手工改的配置文件,对应 faster-whisper 的模型加载和推理参数:{ model_size: small, device: cuda, compute_type: float16, language: zh, beam_size: 5, vad_filter: true, download_root: /models }model_size:模型规格。CPU 机器建议small起步,GPU 显存充足时上medium或large-v3。device:cuda或cpu。容器里跑 GPU 模式,宿主机必须装好 NVIDIA Container Toolkit。compute_type:cuda搭配float16,cpu搭配int8。int8 量化在 CPU 上的加速非常明显,识别质量损失在可接受范围。language:zh会强制按中文识别,不传就让模型自动检测。中文为主的环境建议固定zh,避免中英混排时语言跳来跳去。beam_size:束搜索宽度,默认 5。调到 10 会变慢但偶尔能纠正口误。vad_filter:过滤静音段,减少模型对着长空白生成幻觉文本,推荐打开。download_root:模型缓存目录。离线部署时把模型文件先放到宿主机,通过 Docker volume 挂载进容器。模型规格与资源的对应关系,faster-whisper README 里的公开数据大致如下:模型参数量GPU 显存(约)CPU int8 相对速度适用场景tiny39M~1GB极快简单指令、英文base74M~1GB快清晰普通话可用small244M~2GB中等多数会议、视频转写medium769M~5GB慢口音、噪声环境large-v31550M~10GB很慢高精度字幕模型文件首次使用会自动从 Hugging Face 拉取。社区里用蓝奏云等网盘分享打包好的 whisper 模型压缩包的做法很常见,先下载到本地再挂载进容器,比在构建镜像时反复在线拉取更可控。但要确认解压后的目录结构保持config.json和model.bin的同级关系,faster-whisper 会按目录结构校验。3. 核心实现:whisper_script.py 推理与 main.py 路由3.1 模型加载与音频转写whisper_script.py是这份资源里最值得读的文件。它封装了模型初始化和转写两个职责,对外只暴露一个transcribe函数。常见实现里底层用 faster-whisper 而不是 OpenAI 原版 whisper 包,原因是 faster-whisper 基于 CTranslate2,CPU 上配合 int8 量化比原版快三到五倍,GPU 上 float16 也更省显存:# whisper_script.py import json import threading from faster_whisper import WhisperModel _model None _model_lock threading.Lock() _opts {} def _load_options(pathoptions.json): with open(path, r, encodingutf-8) as f: return json.load(f) def get_model(): global _model if _model is None: with _model_lock: if _model is None: opts _load_options() model_path opts.get(model_path) or opts.get(model_size, small) _model WhisperModel( model_path, deviceopts.get(device, cpu), compute_typeopts.get(compute_type, int8), download_rootopts.get(download_root), ) _opts.update(opts) return _model def transcribe(audio_path, languageNone): model get_model() lang language or _opts.get(language) or None segments, info model.transcribe( audio_path, languagelang, beam_size_opts.get(beam_size, 5), vad_filter_opts.get(vad_filter, True), ) text .join(seg.text for seg in segments).strip() return { text: text, language: info.language, duration: round(info.duration, 2), }代码里两个点需要解释。第一,get_model()里用了两层if _model is None,这是双检锁模式。FastAPI 的异步接口可能在多个 worker 线程里同时进入模型初始化逻辑,只查一次标志位会重复加载模型,浪费内存。第二,segments是生成器而不是列表,faster-whisper为了省内存会逐段产出识别结果,必须在transcribe函数内部把它消费完再返回。如果直接把segments返回给上层,调用方一旦在生成器耗尽前退出,模型的状态会被打断,后续请求会出现难以追踪的异常。info.language和info.duration来自 CTranslate2 的解码元信息,在verbose_json响应里要直接透传给客户端,所以这里一并放进返回值。3.2 FastAPI 路由与多格式响应main.py负责 HTTP 层,核心是/v1/audio/transcriptions这一个路由。实现要点是:文件读入内存、落临时文件、调模型、按格式返回:# main.py import os import tempfile from fastapi import FastAPI, File, Form, HTTPException, UploadFile from whisper_script import transcribe app FastAPI(titleWhisper OpenAI Compatible API) app.post(/v1/audio/transcriptions) async def transcriptions( file: UploadFile File(...), model: str Form(whisper-1), language: str Form(None), response_format: str Form(json), temperature: float Form(0.0), ): content await file.read() if len(content) 25 * 1024 * 1024: raise HTTPException(status_code413, detailaudio file exceeds 25MB) suffix os.path.splitext(file.filename or )[1] with tempfile.NamedTemporaryFile(deleteFalse, suffixsuffix) as tmp: tmp.write(content) tmp_path tmp.name try: result transcribe(tmp_path, languagelanguage) if response_format verbose_json: return { task: transcribe, language: result[language], duration: result[duration], text: result[text], } return {text: result[text]} finally: os.unlink(tmp_path)Form(...)的声明方式告诉 FastAPI 这些字段来自 multipart 表单。file用UploadFile接收,model只是占位参数,OpenAI 的 SDK 会强制传它,服务端不需要用它做任何逻辑。temperature同样只是为了兼容客户端而入参,实际推理不引用。文件之所以要落临时盘,是因为UploadFile本质是内存缓冲,而 faster-whisper 的音频解码器需要真实的文件路径。用tempfile.NamedTemporaryFile时注意deleteFalse,否则文件在句柄关闭时会被直接清掉,后面模型读取不到。finally块里的os.unlink保证请求结束后临时文件不残留,长时间运行下不会把容器磁盘写满。25MB 是 OpenAI 官方接口的体积上限,这里保持同样的边界。超过就直接 413 返回,不浪费 GPU 算力。3.3 依赖清单与高频报错requirements.txt的合理内容如下,刻意不锁版本:fastapi uvicorn[standard] python-multipart faster-whisperuvicorn[standard]会带上 uvloop 和 httptools,比裸 uvicorn 的并发吞吐高一个量级。服务端不需要安装openai包,那是给外部调用方准备的。项目先用最新版本装一遍、跑通接口,再pip freeze requirements.lock.txt锁版本,避免直接抄网上过时的固定版本号。依赖层面的坑集中在这几类:现象大概率原因处理方式所有请求返回 422缺python-multipart装上后重启服务首次请求卡住,随后超时模型正在下载或首次加载看容器日志确认;生产环境预热RuntimeError: Model is not loadeddownload_root目录结构不对确认config.json与模型文件同级ffmpeg not found基础镜像没装 ffmpegDockerfile 里apt-get install ffmpegCUDA out of memory模型规格超过显存换小模型,或compute_type改int8识别结果全是空白音频采样率过低或静音过多检查音频源,开启vad_filter定位这些问题没有捷径,先docker logs看 Python traceback,再决定改配置还是改代码。4. Docker 化部署:CPU 与 GPU 两种启动路径4.1 Dockerfile 构建了哪些基础能力这个项目的 Dockerfile 围绕三个目标写:镜像体积可控、音频解码不缺依赖、日志真实可查。python:3.11-slim作为基础镜像是比较稳妥的起点,faster-whisper 的 wheel 不需要编译,gcc 之类可以省掉;但 ffmpeg 必须装,没有它任何音频格式的解码都会失败:FROM python:3.11-slim ENV PYTHONUNBUFFERED1 \ PIP_NO_CACHE_DIR1 RUN apt-get update apt-get install -y --no-install-recommends ffmpeg \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py whisper_script.py options.json ./ EXPOSE 6008 CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 6008]PYTHONUNBUFFERED1让 Python 的 stdout 不做缓冲,模型加载日志和识别过程中的警告能实时出现在docker logs里,排查问题不用干等。rm -rf /var/lib/apt/lists/*是 apt 装完包之后的常规清理,能把镜像体积压掉几十 MB。PIP_NO_CACHE_DIR1关闭 pip 缓存,避免构建层里塞进无用的安装包。.dockerignore至少要排除这些内容:.idea/ __pycache__/ *.pyc .git/很多人 zip 解压完直接docker build,把.idea和__pycache__一起烤进镜像,既不安全也没必要。.dockerignore文件本身也要和 Dockerfile 放在同一级目录。4.2 build 与 run 命令的参数差异构建和启动命令就是摘要里给的那几条,但每个参数的取舍值得说清楚:# 构建镜像 docker build -t whisper . # GPU 模式启动 docker run -itd --name whisper-api \ -p 6008:6008 \ --gpus all \ --restartalways \ whisper # CPU 模式启动 docker run -itd --name whisper-api \ -p 6008:6008 \ --restartalways \ whisper参数含义对照:参数作用说明-itd交互式 后台-d保证容器在后台运行,-i -t组合便于后续 exec 进去操作-p 6008:6008端口映射宿主机 6008 转容器 6008,Uvicorn 监听容器内端口--gpus all暴露全部 GPU仅 GPU 模式需要,依赖宿主 NVIDIA Container Toolkit--restartalways重启策略容器崩溃或宿主机重启后自动拉起CPU 和 GPU 模式真正的差异在options.json。GPU 模式把device设为cuda、compute_type设为float16;CPU 模式把device设为cpu、compute_type设为int8。如果 GPU 模式跑在没装 NVIDIA Container Toolkit 的机器上,docker run --gpus all会直接报错无法启动;CPU 模式没有这个前置条件。模型文件不要打进镜像。用-v /opt/whisper-models:/models把宿主机目录挂载进容器,options.json里对应的download_root指向/models。这样换模型规格只需替换宿主机文件、重启容器,不用重新构建镜像。端口如果被占,改-p 7008:6008即可,容器内端口不用动。4.3 容器内验证与资源限制启动后先做三件事:看日志、探活、确认推理设备。# 看启动日志,确认模型加载路径和 CUDA 状态 docker logs -f whisper-api # 探活,FastAPI 自带 Swagger 文档页面 curl http://127.0.0.1:6008/docs # 进入容器检查 GPU 是否被 CTranslate2 识别 docker exec whisper-api python -c import ctranslate2; print(ctranslate2.get_cuda_device_count())最后一个命令返回0说明容器内看不到 GPU,返回1或更高说明 CUDA 设备可用。这个检查比nvidia-smi更贴近实际,因为 faster-whisper 用的是 CTranslate2 而不是原生 CUDA 接口。长时间跑的容器建议加上资源限制:docker run -itd --name whisper-api \ -p 6008:6008 \ --gpus all \ --cpus4 \ -m 6g \ --restartalways \ whisper--cpus限制 CPU 核心数,-m限制内存上限。faster-whisper 默认会吃掉机器上所有可用的 CPU 线程来做解码,不加限制时,一个识别请求能让整台服务器的其他服务全部卡顿。--gpus all的机器上如果同时部署了其他 GPU 服务,可以通过--gpus device0只绑定单张卡,避免显存互相挤占。5. 接口验证与扩展:从 curl、OpenAI SDK 到字幕工具联动5.1 curl 与官方 SDK 验证容器起来后,第一件事是用 curl 打一发真实请求,确认整条链路通:curl http://localhost:6008/v1/audio/transcriptions \ -H Authorization: Bearer local-key \ -F filemeeting.mp3 \ -F modelwhisper-1 \ -F languagezh注意-H里的 Authorization 头。本地服务可以不校验密钥,但客户端 SDK 会默认带上,所以服务端至少不要因为这个头返回 401。返回的 JSON 结构应当和 OpenAI 官方一致:{text: 识别出的文本内容}。如果已有业务代码用的是官方 openai SDK,切换成本只有两行:from openai import OpenAI client OpenAI( base_urlhttp://127.0.0.1:6008/v1, api_keylocal-key, ) with open(meeting.mp3, rb) as f: result client.audio.transcriptions.create( modelwhisper-1, filef, languagezh, response_formatverbose_json, ) print(result.text) print(result.language, result.duration)base_url指向本地服务的/v1路径,SDK 会自动拼出/v1/audio/transcriptions。api_key随便填一个占位符,本地服务不校验。response_formatverbose_json时,SDK 返回的对象带language和duration属性,对应main.py里的 verbose_json 分支。5.2 字幕工具与离线模型的接入这个接口最常见的实际消费方,是各类字幕生成工具。Subtitle Edit 的部分版本,以及 PotPlayer 生态里支持自定义语音识别端点的字幕插件,都可以把 Whisper API 地址填成http://127.0.0.1:6008/v1之后直接出字幕。前提是工具本身支持 OpenAI 兼容端点,老版本字幕插件只认官方地址的,没法接本地服务。字幕场景记得在options.json里把language固定成zh,并打开vad_filter。视频里的大段背景音乐会让自动语言检测在中文和英文之间反复横跳,固定语言后识别文本的连贯性会明显改善。模型的 medium 或 large-v3 规格更适合字幕精度要求高的场合,small 在处理带有口音的对话时错字率会明显上升。5.3 并发控制与实时转写的改造点faster-whisper 的单个模型实例同时跑多个转写任务,会竞争 GPU 显存和解码线程池,轻则变慢,重则 CUDA OOM。最常见的解法是给模型调用加全局锁,让请求排队执行:import asyncio from whisper_script import transcribe _transcribe_lock asyncio.Lock() app.post(/v1/audio/transcriptions) async def transcriptions(file, model, language, response_format, temperature): # ... 文件落盘逻辑 ... async with _transcribe_lock: result await asyncio.to_thread(transcribe, tmp_path, languagelanguage)asyncio.to_thread把同步的模型调用丢进线程池,避免阻塞 FastAPI 的事件循环;asyncio.Lock保证任意时刻只有一个转写任务在跑。代价是并发请求会排队,但换来的是可预期的响应时间和不会把显存打爆。想要更高吞吐,就预加载两个模型实例做简单的轮询分发,每个实例配一把锁,这是另一个层面的改造了。实时语音转写是另一个常见扩展方向。把音频流按 3 到 5 秒切片,配合 VAD 判断断句,每个切片走一次transcribe,再把结果按时间戳拼接。这个方案不需要改接口格式,只加一个 WebSocket 端点和切片缓冲,就能让现有兼容层同时服务离线文件和实时流。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价