如果你需要在Ubuntu 24.04上搭一个完全本地的语音识别服务,把录音、播客、会议音频批量转成文本,同时又不想被一堆Python依赖折腾到怀疑人生,那Whisper配合Docker Compose几乎是我目前试过最省心的组合。我最近把整套流程从零跑通,从模型选型到容器编排、GPU透传、接口调用都踩了不少坑,这篇文章就是完整记录,适合想在内网、实验室或者自己家搭建离线语音转写服务的人参考,也适合刚接触Docker和语音模型的初学者直接抄作业。1. 为什么选Docker Compose来部署Whisper,而不是直接裸装1.1 环境隔离与依赖冲突,是最大的理由Whisper这个模型本身很轻,真正折磨人的是它背后的运行环境。模型推理依赖的深度学习框架、音频解码组件、分词器、数值计算库,每一个都可能和系统里的其他软件打架。Ubuntu 24.04默认自带的Python版本较新,很多编译型依赖包在安装时会对GLIBC版本、GCC版本、CUDA工具链有要求。直接pip安装的话,今天为了修某个音频库的编译错误动一次系统Python,明天又为了某个工具的兼容性改一遍环境变量,最后系统怎么坏的都不知道。用Docker Compose之后,整个运行环境被完整封装进镜像:Python版本、ffmpeg版本、依赖库、启动命令全都在镜像里固化下来。宿主机只要装了Docker和对应的GPU容器运行时,就能直接跑,不会污染系统环境。换一台机器部署时,只需要拷贝一份项目目录,一条docker compose up就能拉起同样的服务,不用再逐个确认每个依赖的版本。1.2 Compose解决了“部署一次,到处复现”的问题用Docker单命令也能启动容器,但如果涉及多个服务、多个挂载目录、环境变量、重启策略、健康检查,命令会越来越长。Compose把这些配置收敛到一个YAML文件里,用声明式的方式描述整个服务的运行状态。我这边项目是同时要挂载模型目录、音频输入目录、输出目录,还要限制容器资源、配置健康检查。这些全写在compose里,比写一堆docker run参数清晰得多。而且因为是文本文件,可以直接进版本库,同事拿到项目目录就能复现,不用靠聊天记录里的历史命令。另外一个容易被忽略的收益是升级和回滚。镜像重新构建或者改用不同版本时,compose文件里只改几行,重新up一次就能完成状态切换。相比手工维护一套Python虚拟环境,可操作性高一个量级。我实际测下来,整个容器化流程只要网络正常,从目录创建到服务响应,大概是几分钟的事。2. 部署前必须想清楚的三个关键点2.1 模型尺寸选型,决定你能转出什么质量Whisper官方提供了多个尺寸的模型,这是部署前第一个要决策的点。很多人一上来就直接上大模型,结果资源吃紧、速度慢、调度问题频发,最后反而觉得项目难搞。模型尺寸、资源占用和识别质量之间的关系非常直接,没有免费午餐。模型尺寸参数规模内存占用参考转写速度适用场景tiny约39M1GB以内极快实时性要求高、对精度要求低的场景base约74M1~2GB快简单命令词、短句识别small约244M2~4GB中等日常语音转写、中文普通话medium约769M5~10GB较慢对质量有要求、资源充足的机器large-v3约1550M10GB以上慢追求最高识别质量、不赶时间以中文场景为例,tiny和base在带口音、嘈杂环境下基本不可用,措辞容易被识别成奇怪的内容。small开始能应付大多数日常音频,medium在长音频、多说话人场景下明显更稳,large-v3则适合对字幕质量有硬性要求的场景。如果只是自己整理会议录音,推荐small到medium之间起步;如果是商业质检、字幕组批量生成,直接考虑large-v3配GPU。这里有一个我强烈建议的做法:先用small把整个链路跑通,确认服务正常、接口稳定之后,再切换更大模型。直接上large如果遇到OOM或者加载超时,你都不知道问题出在环境还是模型,排查成本会高很多。2.2 CPU、GPU与量化方式,动手前先定方案同一个模型在不同硬件上的表现差距非常大。先说结论:没有独立显卡的时候,CPU模式加int8量化是最稳妥的组合;有N卡且显存足够的时候,用CUDA加float16是明显更优的选择。CPU部署有一个好处:兼容性极好,几乎任何机器都能跑。但小尺寸模型在CPU上转写一段几分钟的音频,可能需要数倍于音频时长的时间。int8量化能把内存占用和推理速度都优化不少,识别质量损失在small以下几乎可以忽略,在medium级别会有一点点可感知的差异,但通常不影响阅读。GPU部署则需要先确认CUDA环境能被容器访问,否则即使宿主机跑着显卡驱动,容器里也检测不到。这一块的问题我在后面“常见问题”里专门讲。硬件方案一旦确定,compose文件、代码里的设备参数、量化类型都要保持一致,尤其是compute_type设错,轻则速度下降,重则直接加载失败。2.3 镜像选择与依赖清单,别贪图“一行命令跑起来”网上有一些现成的Whisper服务镜像,拉下来就能用,确实方便。但现成镜像的问题在于:你不知道里面编译了什么、版本是否冻结、有没有不必要的组件。镜像体积一大,内网部署拉取本身就耗时。我更推荐基于一个干净的Python基础镜像,自己写几行Dockerfile,把ffmpeg、推理库、Web框架装好。这样镜像虽然也是自己构建的,但每一层都能看懂,后期升级、排查都更可控。依赖清单里最容易出问题的两个:一个是ffmpeg,Whisper依赖它做音频解码,系统里没有或者版本太老会导致转写结果为空;另一个是Hugging Face相关的下载组件,它负责模型缓存和加载。在容器构建阶段就把这些装好,比在运行时才发现缺依赖要省心得多。3. 从零开始的完整部署过程3.1 前置准备:装Docker、建立目录结构、准备模型文件先确认Docker环境。Ubuntu 24.04可以直接用发行版自带的包,也可以从Docker官方仓库装,两者区别不大。我自己用的是发行版自带的:sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER安装结束后,重新登录一次让用户组生效。验证一下:docker info docker compose version不需要sudo就能执行docker命令说明当前用户已经在docker组里生效了。然后设计目录结构。我这边项目用的是这样的布局:/opt/whisper-local/ ├── compose.yaml ├── Dockerfile ├── requirements.txt ├── app.py ├── models/ │ └── faster-whisper-small/ ├── audio/ └── output/audio目录放待转写的录音,output目录放转写结果,models目录放模型文件。最关键的是模型文件不要等到容器启动时再现场从网络下载,而是提前下载到models目录里,容器启动时直接挂载进去。这样既避免了运行时下载慢、超时的问题,也让整个服务完全离线可用。下载模型我用的是模型仓库的CLI工具,命令大致如下:pip install -U huggingface_hub huggingface-cli download 模型ID --local-dir ./models/faster-whisper-small模型ID占位部分根据你选择的模型尺寸填写。下载完成后,确认models/faster-whisper-small目录下包含config.json、model.bin、tokenizer.json等完整文件再启动服务。这一步值得多花几分钟检查,我遇到过模型文件下了一半就强行启动的情况,容器日志里一直是加载失败的报错。3.2 写一个能直接用的compose.yaml在项目根目录创建compose.yaml,这是整个服务的核心描述文件。以下是我实际在用的版本,参数都标注了用途:name: whisper-local services: whisper-server: build: . image: whisper-server:local container_name: whisper-local restart: unless-stopped ports: - "8000:8000" volumes: - ./models:/models:ro - ./audio:/audio - ./output:/output - whisper-cache:/root/.cache environment: - WHISPER_MODEL_DIR=/models/faster-whisper-small - DEVICE=cpu - COMPUTE_TYPE=int8 - WHISPER_LANGUAGE=zh mem_limit: 6g cpus: "4.0" healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:8000/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 120s volumes: whisper-cache:解释几个关键配置:restart: unless-stopped:系统重启、Docker重启后服务自动拉起,适合无人值守的转录服务。ports: 8000:8000:宿主机8000端口映射到容器内8000端口,后续局域网内其他机器也能通过这台机器的IP访问。whisper-cache:命名卷,避免镜像重建时模型重复缓存下载。虽然我们手动挂载了模型目录,但推理库可能还会在/root/.cache下写一些临时缓存,保留这个卷能加速二次启动。mem_limit和cpus:给容器设资源上限,防止一次并发转写把整台机器拖垮。数值根据你实际机器内存调整,如果部署large模型,建议mem_limit至少16g。start_period: 120s:模型加载需要时间,给健康检查一个宽限期,不然刚启动就被判定为不健康,还会反复重启。3.3 准备镜像与后端代码在项目根目录创建Dockerfile:FROM python:3.11-slim RUN apt-get update && apt-get install -y ffmpeg curl && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]requirements.txt内容如下:faster-whisper fastapi uvicorn python-multipart这里之所以用faster-whisper而不是原始的Whisper,是因为它基于CTranslate2重新实现了推理部分,在CPU和GPU上的速度都有明显提升,内存占用也更低。API返回的转写结果我们只需要拼接即可,代码里可以保持简单。后端接口文件app.py,我写了一个精简但干净可用的版本:import os import tempfile from fastapi import FastAPI, File, UploadFile from faster_whisper import WhisperModel MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", "/models/faster-whisper-small") DEVICE = os.getenv("DEVICE", "cpu") COMPUTE_TYPE = os.getenv("COMPUTE_TYPE", "int8") LANGUAGE = os.getenv("WHISPER_LANGUAGE", "zh") app = FastAPI() model = WhisperModel(MODEL_DIR, device=DEVICE, compute_type=COMPUTE_TYPE) @app.get("/health") async def health(): return {"status": "ok", "model": MODEL_DIR} @app.post("/transcribe") async def transcribe(file: UploadFile = File(...), language: str = LANGUAGE): suffix = os.path.splitext(file.filename or "audio.wav")[1] or ".wav" with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: tmp.write(await file.read()) tmp_path = tmp.name segments, info = model.transcribe( tmp_path, language=language, beam_size=2, vad_filter=True, ) text = "".join(seg.text for seg in segments) os.unlink(tmp_path) return { "text": text, "language": info.language, "duration": round(info.duration, 2), }这里要注意一个问题:model.transcribe返回的是一个生成器,遍历segments时模型才真正执行推理。所以不要在前面打印或测量生成器,而是在遍历完后再取info.duration,否则可能出现时长信息和文本不匹配的诡异现象。我一开始在这个地方踩过坑,以为模型没跑完,其实是生成器还没被消耗。3.4 启动容器、测试转写接口、验证服务健康目录结构准备好后,在项目根目录执行:docker compose up -d --build第一次构建会比较慢,因为要拉基础镜像、装依赖。构建完成后查看容器状态:docker compose ps docker compose logs -f看到Started server process或者健康检查通过后,先用curl验证服务存活:curl -s http://127.0.0.1:8000/health返回{"status":"ok","model":"/models/faster-whisper-small"}说明容器正常。然后准备一段测试音频。注意Whisper对音频格式的要求其实并不挑剔,但为了减少前端出错的概率,我习惯先把待转写文件转成16kHz采样率、单声道的WAV:ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le test.wav然后调用转写接口:curl -X POST http://127.0.0.1:8000/transcribe \ -F "file=@./test.wav" \ -F "language=zh"返回结果类似:{"text":"今天下午的会议讨论了三件事,第一是容器化部署的进度……","language":"zh","duration":15.36}能拿到文本就说明整套链路通了。如果返回空文本,大概率是音频解码问题,优先检查容器内ffmpeg是否正常工作:docker exec whisper-local ffmpeg -version3.5 精度与并发参数调优服务跑通之后,面临的真实问题是:转写结果能不能更准、并发请求会不会把机器打爆。我的调优顺序是这样的:先固定语言,再调beam size,最后决定是否启用VAD过滤。固定语言对速度的提升非常明显。如果你知道自己处理的音频都是中文,就不要依赖自动语言检测,直接把language=zh传进去。自动检测不仅要额外推理一轮,还有可能误判,特别是在音乐、静音较多的场景里。beam_size是Beam Search的宽度。默认值对应速度优先,调高到5-8会提升精度,但推理时间也会上涨。我实测下来,对于普通话清晰、无背景音的录音,beam_size设为2和5的差别不大;但对方言口音或嘈杂环境,调高beam_size能看到明显的文字连贯性改善。VAD(语音活动检测)是容易被忽略的好功能。它能在音频中过滤掉静音段和人声间隙,避免模型在空白片段上产生幻觉文本。尤其是长音频里夹杂大段空白时,开启VAD能显著降低输出中出现无意义重复内容的概率。代价是会增加一些前处理时间,整体来说值得开启。并发方面,faster-whisper的模型加载后,每个转写请求都会占用一定内存,CPU环境下多个请求同时到达会相互争抢算力,实际吞吐反而更差。建议在业务侧控制并发数,或者在前端加一个简单队列,保证同时只有一个转写任务。有条件的话,在compose里给容器限制cpus,让Docker调度器帮忙控制资源竞争。4. 实操中高频出现的问题与排查4.1 容器里GPU不可见,容器启动就报错典型症状:宿主机上nvidia-smi正常,但容器内报找不到CUDA设备,或者服务启动时模型加载失败。这个问题的根源几乎都是GPU容器运行时没装好,而不是代码问题。先验证容器能否访问GPU:docker run --rm --gpus all <你的基础镜像> nvidia-smi如果提示找不到GPU,说明需要安装N卡容器工具包:sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker重启Docker后,再跑上面的验证命令。能正常输出GPU信息后,把compose里环境变量改成DEVICE=cuda、COMPUTE_TYPE=float16,重新构建启动即可。注意宿主机显卡驱动版本要足够新,否则即使容器运行时装好了,驱动接口不匹配一样会报错。4.2 模型反复下载、下载慢一种很常见的情况是:明明本机已经有模型文件了,容器启动时还是往模型仓库下载几百MB甚至几个GB的数据。原因在于模型路径配置不对。WhisperModel接收的是一个本地目录路径时,它就直接从本地加载;接收的是字符串模型名时,它会尝试从模型仓库拉取最新文件。所以compose里的WHISPER_MODEL_DIR必须指向挂载目录下的具体子目录,而不是草草写一个/models了事。下载慢的问题,我的建议是永远不要在容器运行时临时下载大模型。提前把模型下好、放到models目录里挂载进容器,是最稳妥的方案。如果你有局域网内的其他机器已经下载过模型,直接拷贝过来即可。容器启动后只要日志里没有关于模型仓库访问的记录,说明走得是本地加载。4.3 中文乱码、JSON转义与编码问题转写结果里中文正常,但终端打印出来是\uXXXX一类的转义序列,或者日志里全是乱码。这种情况通常有两层原因:一是容器内没有设置UTF-8语言环境,二是在调用接口拿结果时没有正确解码。在compose的environment里加上LANG=C.UTF-8和LC_ALL=C.UTF-8,可以让容器内日志和文件名处理更规范。接口层我建议在FastAPI返回JSON时保持中文原文,不需要额外配置,因为HTTP层面的UTF-8编码本来就是默认的。如果客户端是脚本,请求响应后一定要按response.encoding或utf-8解码,不要用系统默认编码,尤其是Windows终端下容易乱码。4.4 内存不足、长音频转写卡死长音频转写时容器内存飙升,甚至直接被OOM杀掉,这是CPU部署里最常遇到的问题。原因是整段音频会一次性加载到内存里做特征提取,而模型推理过程中还会额外占用大量临时缓冲。我在一台内存只有8G的机器上跑medium模型,处理半小时的音频,内存占用直接突破5G。解决办法很直接:长音频先切分再转写。用ffmpeg把音频按60秒一段切成小文件,逐段调用转写接口,最后按时间戳拼接。Whisper对短音频的处理速度更快,分段后内存峰值也大幅下降。另一种思路是限制容器内存上限,让它因内存不足提前报错而不是悄悄拖垮宿主机。compose里mem_limit字段是垃圾回收的第一道防线。4.5 容器起不来、端口冲突与调参方向服务偶尔出现“端口已被占用”或者容器反复重启,排查顺序我建议是:先看日志,再看端口占用,最后看健康检查。docker compose logs --tail 100 sudo lsof -i :8000如果是端口被旧容器占用,执行docker compose down清理后再up。如果日志显示模型加载失败,先去确认models目录挂载路径和实际文件名是否一致。如果容器反复restart但日志又是空的,多半是健康检查在模型加载完成之前就开始探测,把start_period调大一点即可。还有一个容易被忽略的问题:docker compose和docker-compose是两个不同的命令。旧版用连字符,新版用空格。Ubuntu 24.04安装的Compose插件正确用法是docker compose,如果你发现输入旧命令报错,不用惊讶,换空格版就行。整套部署跑下来,我最想提醒的不是某个命令怎么写,而是把“模型下载、模型加载、接口调用”这三个阶段拆开看。每个阶段独立验证通过后再进下一步,能省掉大量串行排查的时间。我在实际操作中最满意的一个改动是把模型目录独立挂载、和代码完全分离,后续换模型尺寸时只需要改环境变量指向新目录,不需要重新构建镜像,这个模式建议直接保留。部署起来之后,日常使用中调整最多的其实就是beam size和VAD开关,其他参数基本可以稳定不动。
如果你需要在Ubuntu 24.04上搭一个完全本地的语音识别服务,把录音、播客、会议音频批量转成文本,同时又不想被一堆Python依赖折腾到怀疑人生,那Whisper配合Docker Compose几乎是我目前试过最省心的组合。我最近把整套流程从零跑通,从模型选型到容器编排、GPU透传、接口调用都踩了不少坑,这篇文章就是完整记录,适合想在内网、实验室或者自己家搭建离线语音转写服务的人参考,也适合刚接触Docker和语音模型的初学者直接抄作业。1. 为什么选Docker Compose来部署Whisper,而不是直接裸装1.1 环境隔离与依赖冲突,是最大的理由Whisper这个模型本身很轻,真正折磨人的是它背后的运行环境。模型推理依赖的深度学习框架、音频解码组件、分词器、数值计算库,每一个都可能和系统里的其他软件打架。Ubuntu 24.04默认自带的Python版本较新,很多编译型依赖包在安装时会对GLIBC版本、GCC版本、CUDA工具链有要求。直接pip安装的话,今天为了修某个音频库的编译错误动一次系统Python,明天又为了某个工具的兼容性改一遍环境变量,最后系统怎么坏的都不知道。用Docker Compose之后,整个运行环境被完整封装进镜像:Python版本、ffmpeg版本、依赖库、启动命令全都在镜像里固化下来。宿主机只要装了Docker和对应的GPU容器运行时,就能直接跑,不会污染系统环境。换一台机器部署时,只需要拷贝一份项目目录,一条docker compose up就能拉起同样的服务,不用再逐个确认每个依赖的版本。1.2 Compose解决了“部署一次,到处复现”的问题用Docker单命令也能启动容器,但如果涉及多个服务、多个挂载目录、环境变量、重启策略、健康检查,命令会越来越长。Compose把这些配置收敛到一个YAML文件里,用声明式的方式描述整个服务的运行状态。我这边项目是同时要挂载模型目录、音频输入目录、输出目录,还要限制容器资源、配置健康检查。这些全写在compose里,比写一堆docker run参数清晰得多。而且因为是文本文件,可以直接进版本库,同事拿到项目目录就能复现,不用靠聊天记录里的历史命令。另外一个容易被忽略的收益是升级和回滚。镜像重新构建或者改用不同版本时,compose文件里只改几行,重新up一次就能完成状态切换。相比手工维护一套Python虚拟环境,可操作性高一个量级。我实际测下来,整个容器化流程只要网络正常,从目录创建到服务响应,大概是几分钟的事。2. 部署前必须想清楚的三个关键点2.1 模型尺寸选型,决定你能转出什么质量Whisper官方提供了多个尺寸的模型,这是部署前第一个要决策的点。很多人一上来就直接上大模型,结果资源吃紧、速度慢、调度问题频发,最后反而觉得项目难搞。模型尺寸、资源占用和识别质量之间的关系非常直接,没有免费午餐。模型尺寸参数规模内存占用参考转写速度适用场景tiny约39M1GB以内极快实时性要求高、对精度要求低的场景base约74M1~2GB快简单命令词、短句识别small约244M2~4GB中等日常语音转写、中文普通话medium约769M5~10GB较慢对质量有要求、资源充足的机器large-v3约1550M10GB以上慢追求最高识别质量、不赶时间以中文场景为例,tiny和base在带口音、嘈杂环境下基本不可用,措辞容易被识别成奇怪的内容。small开始能应付大多数日常音频,medium在长音频、多说话人场景下明显更稳,large-v3则适合对字幕质量有硬性要求的场景。如果只是自己整理会议录音,推荐small到medium之间起步;如果是商业质检、字幕组批量生成,直接考虑large-v3配GPU。这里有一个我强烈建议的做法:先用small把整个链路跑通,确认服务正常、接口稳定之后,再切换更大模型。直接上large如果遇到OOM或者加载超时,你都不知道问题出在环境还是模型,排查成本会高很多。2.2 CPU、GPU与量化方式,动手前先定方案同一个模型在不同硬件上的表现差距非常大。先说结论:没有独立显卡的时候,CPU模式加int8量化是最稳妥的组合;有N卡且显存足够的时候,用CUDA加float16是明显更优的选择。CPU部署有一个好处:兼容性极好,几乎任何机器都能跑。但小尺寸模型在CPU上转写一段几分钟的音频,可能需要数倍于音频时长的时间。int8量化能把内存占用和推理速度都优化不少,识别质量损失在small以下几乎可以忽略,在medium级别会有一点点可感知的差异,但通常不影响阅读。GPU部署则需要先确认CUDA环境能被容器访问,否则即使宿主机跑着显卡驱动,容器里也检测不到。这一块的问题我在后面“常见问题”里专门讲。硬件方案一旦确定,compose文件、代码里的设备参数、量化类型都要保持一致,尤其是compute_type设错,轻则速度下降,重则直接加载失败。2.3 镜像选择与依赖清单,别贪图“一行命令跑起来”网上有一些现成的Whisper服务镜像,拉下来就能用,确实方便。但现成镜像的问题在于:你不知道里面编译了什么、版本是否冻结、有没有不必要的组件。镜像体积一大,内网部署拉取本身就耗时。我更推荐基于一个干净的Python基础镜像,自己写几行Dockerfile,把ffmpeg、推理库、Web框架装好。这样镜像虽然也是自己构建的,但每一层都能看懂,后期升级、排查都更可控。依赖清单里最容易出问题的两个:一个是ffmpeg,Whisper依赖它做音频解码,系统里没有或者版本太老会导致转写结果为空;另一个是Hugging Face相关的下载组件,它负责模型缓存和加载。在容器构建阶段就把这些装好,比在运行时才发现缺依赖要省心得多。3. 从零开始的完整部署过程3.1 前置准备:装Docker、建立目录结构、准备模型文件先确认Docker环境。Ubuntu 24.04可以直接用发行版自带的包,也可以从Docker官方仓库装,两者区别不大。我自己用的是发行版自带的:sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER安装结束后,重新登录一次让用户组生效。验证一下:docker info docker compose version不需要sudo就能执行docker命令说明当前用户已经在docker组里生效了。然后设计目录结构。我这边项目用的是这样的布局:/opt/whisper-local/ ├── compose.yaml ├── Dockerfile ├── requirements.txt ├── app.py ├── models/ │ └── faster-whisper-small/ ├── audio/ └── output/audio目录放待转写的录音,output目录放转写结果,models目录放模型文件。最关键的是模型文件不要等到容器启动时再现场从网络下载,而是提前下载到models目录里,容器启动时直接挂载进去。这样既避免了运行时下载慢、超时的问题,也让整个服务完全离线可用。下载模型我用的是模型仓库的CLI工具,命令大致如下:pip install -U huggingface_hub huggingface-cli download 模型ID --local-dir ./models/faster-whisper-small模型ID占位部分根据你选择的模型尺寸填写。下载完成后,确认models/faster-whisper-small目录下包含config.json、model.bin、tokenizer.json等完整文件再启动服务。这一步值得多花几分钟检查,我遇到过模型文件下了一半就强行启动的情况,容器日志里一直是加载失败的报错。3.2 写一个能直接用的compose.yaml在项目根目录创建compose.yaml,这是整个服务的核心描述文件。以下是我实际在用的版本,参数都标注了用途:name: whisper-local services: whisper-server: build: . image: whisper-server:local container_name: whisper-local restart: unless-stopped ports: - "8000:8000" volumes: - ./models:/models:ro - ./audio:/audio - ./output:/output - whisper-cache:/root/.cache environment: - WHISPER_MODEL_DIR=/models/faster-whisper-small - DEVICE=cpu - COMPUTE_TYPE=int8 - WHISPER_LANGUAGE=zh mem_limit: 6g cpus: "4.0" healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:8000/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 120s volumes: whisper-cache:解释几个关键配置:restart: unless-stopped:系统重启、Docker重启后服务自动拉起,适合无人值守的转录服务。ports: 8000:8000:宿主机8000端口映射到容器内8000端口,后续局域网内其他机器也能通过这台机器的IP访问。whisper-cache:命名卷,避免镜像重建时模型重复缓存下载。虽然我们手动挂载了模型目录,但推理库可能还会在/root/.cache下写一些临时缓存,保留这个卷能加速二次启动。mem_limit和cpus:给容器设资源上限,防止一次并发转写把整台机器拖垮。数值根据你实际机器内存调整,如果部署large模型,建议mem_limit至少16g。start_period: 120s:模型加载需要时间,给健康检查一个宽限期,不然刚启动就被判定为不健康,还会反复重启。3.3 准备镜像与后端代码在项目根目录创建Dockerfile:FROM python:3.11-slim RUN apt-get update && apt-get install -y ffmpeg curl && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]requirements.txt内容如下:faster-whisper fastapi uvicorn python-multipart这里之所以用faster-whisper而不是原始的Whisper,是因为它基于CTranslate2重新实现了推理部分,在CPU和GPU上的速度都有明显提升,内存占用也更低。API返回的转写结果我们只需要拼接即可,代码里可以保持简单。后端接口文件app.py,我写了一个精简但干净可用的版本:import os import tempfile from fastapi import FastAPI, File, UploadFile from faster_whisper import WhisperModel MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", "/models/faster-whisper-small") DEVICE = os.getenv("DEVICE", "cpu") COMPUTE_TYPE = os.getenv("COMPUTE_TYPE", "int8") LANGUAGE = os.getenv("WHISPER_LANGUAGE", "zh") app = FastAPI() model = WhisperModel(MODEL_DIR, device=DEVICE, compute_type=COMPUTE_TYPE) @app.get("/health") async def health(): return {"status": "ok", "model": MODEL_DIR} @app.post("/transcribe") async def transcribe(file: UploadFile = File(...), language: str = LANGUAGE): suffix = os.path.splitext(file.filename or "audio.wav")[1] or ".wav" with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp: tmp.write(await file.read()) tmp_path = tmp.name segments, info = model.transcribe( tmp_path, language=language, beam_size=2, vad_filter=True, ) text = "".join(seg.text for seg in segments) os.unlink(tmp_path) return { "text": text, "language": info.language, "duration": round(info.duration, 2), }这里要注意一个问题:model.transcribe返回的是一个生成器,遍历segments时模型才真正执行推理。所以不要在前面打印或测量生成器,而是在遍历完后再取info.duration,否则可能出现时长信息和文本不匹配的诡异现象。我一开始在这个地方踩过坑,以为模型没跑完,其实是生成器还没被消耗。3.4 启动容器、测试转写接口、验证服务健康目录结构准备好后,在项目根目录执行:docker compose up -d --build第一次构建会比较慢,因为要拉基础镜像、装依赖。构建完成后查看容器状态:docker compose ps docker compose logs -f看到Started server process或者健康检查通过后,先用curl验证服务存活:curl -s http://127.0.0.1:8000/health返回{"status":"ok","model":"/models/faster-whisper-small"}说明容器正常。然后准备一段测试音频。注意Whisper对音频格式的要求其实并不挑剔,但为了减少前端出错的概率,我习惯先把待转写文件转成16kHz采样率、单声道的WAV:ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le test.wav然后调用转写接口:curl -X POST http://127.0.0.1:8000/transcribe \ -F "file=@./test.wav" \ -F "language=zh"返回结果类似:{"text":"今天下午的会议讨论了三件事,第一是容器化部署的进度……","language":"zh","duration":15.36}能拿到文本就说明整套链路通了。如果返回空文本,大概率是音频解码问题,优先检查容器内ffmpeg是否正常工作:docker exec whisper-local ffmpeg -version3.5 精度与并发参数调优服务跑通之后,面临的真实问题是:转写结果能不能更准、并发请求会不会把机器打爆。我的调优顺序是这样的:先固定语言,再调beam size,最后决定是否启用VAD过滤。固定语言对速度的提升非常明显。如果你知道自己处理的音频都是中文,就不要依赖自动语言检测,直接把language=zh传进去。自动检测不仅要额外推理一轮,还有可能误判,特别是在音乐、静音较多的场景里。beam_size是Beam Search的宽度。默认值对应速度优先,调高到5-8会提升精度,但推理时间也会上涨。我实测下来,对于普通话清晰、无背景音的录音,beam_size设为2和5的差别不大;但对方言口音或嘈杂环境,调高beam_size能看到明显的文字连贯性改善。VAD(语音活动检测)是容易被忽略的好功能。它能在音频中过滤掉静音段和人声间隙,避免模型在空白片段上产生幻觉文本。尤其是长音频里夹杂大段空白时,开启VAD能显著降低输出中出现无意义重复内容的概率。代价是会增加一些前处理时间,整体来说值得开启。并发方面,faster-whisper的模型加载后,每个转写请求都会占用一定内存,CPU环境下多个请求同时到达会相互争抢算力,实际吞吐反而更差。建议在业务侧控制并发数,或者在前端加一个简单队列,保证同时只有一个转写任务。有条件的话,在compose里给容器限制cpus,让Docker调度器帮忙控制资源竞争。4. 实操中高频出现的问题与排查4.1 容器里GPU不可见,容器启动就报错典型症状:宿主机上nvidia-smi正常,但容器内报找不到CUDA设备,或者服务启动时模型加载失败。这个问题的根源几乎都是GPU容器运行时没装好,而不是代码问题。先验证容器能否访问GPU:docker run --rm --gpus all <你的基础镜像> nvidia-smi如果提示找不到GPU,说明需要安装N卡容器工具包:sudo apt install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker重启Docker后,再跑上面的验证命令。能正常输出GPU信息后,把compose里环境变量改成DEVICE=cuda、COMPUTE_TYPE=float16,重新构建启动即可。注意宿主机显卡驱动版本要足够新,否则即使容器运行时装好了,驱动接口不匹配一样会报错。4.2 模型反复下载、下载慢一种很常见的情况是:明明本机已经有模型文件了,容器启动时还是往模型仓库下载几百MB甚至几个GB的数据。原因在于模型路径配置不对。WhisperModel接收的是一个本地目录路径时,它就直接从本地加载;接收的是字符串模型名时,它会尝试从模型仓库拉取最新文件。所以compose里的WHISPER_MODEL_DIR必须指向挂载目录下的具体子目录,而不是草草写一个/models了事。下载慢的问题,我的建议是永远不要在容器运行时临时下载大模型。提前把模型下好、放到models目录里挂载进容器,是最稳妥的方案。如果你有局域网内的其他机器已经下载过模型,直接拷贝过来即可。容器启动后只要日志里没有关于模型仓库访问的记录,说明走得是本地加载。4.3 中文乱码、JSON转义与编码问题转写结果里中文正常,但终端打印出来是\uXXXX一类的转义序列,或者日志里全是乱码。这种情况通常有两层原因:一是容器内没有设置UTF-8语言环境,二是在调用接口拿结果时没有正确解码。在compose的environment里加上LANG=C.UTF-8和LC_ALL=C.UTF-8,可以让容器内日志和文件名处理更规范。接口层我建议在FastAPI返回JSON时保持中文原文,不需要额外配置,因为HTTP层面的UTF-8编码本来就是默认的。如果客户端是脚本,请求响应后一定要按response.encoding或utf-8解码,不要用系统默认编码,尤其是Windows终端下容易乱码。4.4 内存不足、长音频转写卡死长音频转写时容器内存飙升,甚至直接被OOM杀掉,这是CPU部署里最常遇到的问题。原因是整段音频会一次性加载到内存里做特征提取,而模型推理过程中还会额外占用大量临时缓冲。我在一台内存只有8G的机器上跑medium模型,处理半小时的音频,内存占用直接突破5G。解决办法很直接:长音频先切分再转写。用ffmpeg把音频按60秒一段切成小文件,逐段调用转写接口,最后按时间戳拼接。Whisper对短音频的处理速度更快,分段后内存峰值也大幅下降。另一种思路是限制容器内存上限,让它因内存不足提前报错而不是悄悄拖垮宿主机。compose里mem_limit字段是垃圾回收的第一道防线。4.5 容器起不来、端口冲突与调参方向服务偶尔出现“端口已被占用”或者容器反复重启,排查顺序我建议是:先看日志,再看端口占用,最后看健康检查。docker compose logs --tail 100 sudo lsof -i :8000如果是端口被旧容器占用,执行docker compose down清理后再up。如果日志显示模型加载失败,先去确认models目录挂载路径和实际文件名是否一致。如果容器反复restart但日志又是空的,多半是健康检查在模型加载完成之前就开始探测,把start_period调大一点即可。还有一个容易被忽略的问题:docker compose和docker-compose是两个不同的命令。旧版用连字符,新版用空格。Ubuntu 24.04安装的Compose插件正确用法是docker compose,如果你发现输入旧命令报错,不用惊讶,换空格版就行。整套部署跑下来,我最想提醒的不是某个命令怎么写,而是把“模型下载、模型加载、接口调用”这三个阶段拆开看。每个阶段独立验证通过后再进下一步,能省掉大量串行排查的时间。我在实际操作中最满意的一个改动是把模型目录独立挂载、和代码完全分离,后续换模型尺寸时只需要改环境变量指向新目录,不需要重新构建镜像,这个模式建议直接保留。部署起来之后,日常使用中调整最多的其实就是beam size和VAD开关,其他参数基本可以稳定不动。