我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

用Python+Whisper构建本地音频视频转文字转换器

用Python+Whisper构建本地音频视频转文字转换器 简介这是一份基于Whisper模型的音频视频转文字工具源码适合需要批量转录音视频内容的开发者、研究者以及正在学习语音识别技术的Python使用者。项目结构完整包含UI入口、核心转写逻辑、公共工具模块、依赖说明和演示截图能从导入音视频到输出文本展示出完整的调用链路。其中main.py为程序入口tools模块处理音频预处理common_util提供通用辅助函数readme.txt与requirements.txt说明运行环境和依赖便于快速在本地搭建环境并启动。压缩包共23个文件以Python源码负责音频读取与模型调用、XML配置界面与工程参数、pyc编译缓存、TXT/readme说明文档为主要类型整体仅156KB体积轻量、目录清晰。目前已有409人学习下载既可在本地运行体验Whisper的转写效果也可作为二次开发、课程设计或语音转写项目入门的参考。1. Whisper 把音频转文字不是新事为什么还要自己写一套 Python 源码在线语音转文字工具遍地都是但一旦你手上有一批几十个小时的访谈录音、课程录像或者会议纪要逐条上传到网页再等结果会让人怀疑人生。基于 Whisper 模型做本地音频视频转文字转换器解决的正是这个痛点不依赖网络、不按分钟计费、隐私数据不出门还能把视频直接拖进去出带时间戳的逐字稿。这套 Python 设计源码的核心就是帮你把“视频→音频→文字”这条链路封装成命令行或服务批量跑、断点续传、随时换模型。适合谁文案工作者、做播客剪辑的后期、需要整理采访素材的研究者以及想低成本给视频加字幕的开发者。它能省下的不只是钱更是不确定性和等待。2. 先把模型选对Whisper 的模型尺寸、运行环境与最小调用2.1 Whisper 五种模型尺寸怎么选速度和精度的取舍Whisper 官方把模型按参数规模分成了 tiny、base、small、medium、large 五档外加一个 large-v3 的迭代版本。很多人第一次跑就直接上 large结果发现 4G 显存的卡根本放不下或者转一段 10 分钟的视频要等半小时。选模型不是在选“最好的”而是在选“当前硬件能接受的最高精度”。我一般这样拍板CPU 机器或者显存小于 4G用 base 或 small8G 显存以上再考虑 medium如果想做字幕级质量且不着急才选 large。下图是官方 README 里那个耳熟能详的对照表的实际体感tiny 转英文快得离谱但中文识别基本没法用base 能听清普通话短句但遇到方言、专业名词、背景音乐就开始胡说small 是 CPU 能跑的性价比上限中文错字率可控medium 在 GPU 上能处理大部分音视频转文字场景但对于“的、地、得”这类同音字还是偶尔翻车large 适合追求逐字稿质量、愿意等的人。实际项目里最稳的做法是用 small 先跑通全流程确认代码、依赖、输出格式都没问题再针对具体语料测一下 medium 和 large 的识别差异。不要一上来就追求最优模型否则你会花一晚上在下载权重和调显存上而不是在真正要转的文字上。2.2 本地部署 whisper 服务Python 环境与 torch 安装要点要把 Whisper 在本地部署成服务第一步不是 pip install openai-whisper而是先把 Python 和 PyTorch 的版本对齐。Whisper 依赖 torch而 torch 又强依赖 CUDA 版本。很多“装了库但 import 就崩”的问题几乎都是 torch 装成了 CPU 版或者 CUDA 11 的 torch 配了 CUDA 12 的驱动。常见的做法是用 conda 建一个干净环境conda create -n whisper python3.10 -y conda activate whisper pip install --upgrade pip # 如果只用 CPU装 CPU 版 torch体积小且不会报 CUDA 错 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # 如果有 NVIDIA 显卡先查自己的 CUDA 版本再装对应版本 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install openai-whisper这段命令的逻辑是先隔离环境避免污染系统 Python再根据硬件选择 torch 的安装源。注意--index-url是指定 PyTorch 官方源不是下载页而是让 pip 从这个源里拉对应 CUDA 版本的 wheel 包。如果是 macOS直接pip install torch即可但 MPS 加速在 Whisper 上仍有兼容问题建议还是当 CPU 用。参数说明python3.10是因为 torch 对 3.11/3.12 的支持曾有一段时间不完善虽然现在问题少了但 3.10 仍然是社区里最稳的选择。cu118表示 CUDA 11.8 的预编译包如果你的显卡驱动支持 CUDA 12也可以换cu121但必须保证驱动版本足够新。装完这些后跑python -c import whisper; print(whisper.__version__)能正常输出说明基础环境已经通了。2.3 验证模型跑通的第一段代码transcribe() 最小调用环境就绪后先不要写复杂的转换器用一段最小代码验证模型能加载、能推理、能输出文字。这一步的目的不是处理业务而是确认整个链路没有硬件层面的黑匣子问题。import whisper # 加载模型返回的是一个 Whisper 实例 model whisper.load_model(small) # 转写一个音频文件 result model.transcribe(test.mp3, languagezh, fp16False) # 打印识别结果 print(result[text])这段代码看似简单但有两个参数值得说明。languagezh是让模型固定用中文解码而不是自动检测语种。自动检测对纯英文没问题但中文语音经常被误判成日文或粤语导致输出一堆汉字夹杂假名。fp16False强制关闭半精度推理这在 CPU 上是必须的否则会报数据类型错误在 GPU 上如果要开启半精度提速可以改成fp16True但前提是 CUDA 版本和 torch 都支持。跑通这段代码后你已经有了一个“能用”的 Whisper 转文字原型。但离“音频视频转文字转换器”还差两步一是让程序能直接接收视频文件二是把转写流程封装成可复用的代码结构。下一章我会把这两步补上。3. 从视频到文字ffmpeg 音频抽取与 Whisper 转录的完整流程3.1 为什么一定要先抽音频Whisper 不直接读视频文件Whisper 的transcribe()虽然接受文件路径作为参数但实际上它只认音频格式。如果你直接传一个.mp4进去OpenAI 的官方代码会尝试用 ffmpeg 读取音频流但前提是你的系统里装了 ffmpeg 并且路径可用。更麻烦的是直接传视频时Whisper 内部会把整个视频文件当作媒体流处理遇到带画面压缩的复杂视频读取耗时剧增甚至出现“音频读出来了但画面解码卡死”的情况。所以我在设计转换器时总把“音频抽取”做成独立的一步而不是依赖 Whisper 内部隐式调用 ffmpeg。这样做有三个好处第一抽取后的音频可以统一为 16kHz 单声道 wav这是 Whisper 官方推荐的输入格式识别效果最稳定第二抽音频和转文字可以分开调试哪个环节失败一眼就能看到第三后续如果换成其他语音识别引擎音频缓存还能复用。3.2 用 ffmpeg 把 mp4 转成 16kHz wav命令与参数说明ffmpeg 是音频视频处理的事实标准但很多人只是听说过没认真理解过它的参数。转换器里最常用的命令是这一个ffmpeg -i input.mp4 -vn -ac 1 -ar 16000 -f wav output.wav-i指定输入文件-vn表示不要视频流只处理音频。-ac 1把声道合并成单声道这非常重要Whisper 对立体声处理不好如果两个声道内容不一致会识别出重复文本。-ar 16000重采样到 16000 Hz这是语音识别领域最常见的采样率既不丢失语音细节又不会让计算量白白翻倍。-f wav强制输出 wav 封装格式避免生成其他容器导致后续读取失败。在 Python 里调用这条命令我不会用os.system而是用subprocess.run并捕获错误输出因为 ffmpeg 的报错信息经常能直接告诉你音频流损坏还是编码不支持import subprocess def extract_audio(video_path, audio_path): cmd [ ffmpeg, -i, video_path, -vn, -ac, 1, -ar, 16000, -f, wav, -y, audio_path ] proc subprocess.run(cmd, capture_outputTrue, textTrue) if proc.returncode ! 0: raise RuntimeError(fffmpeg 抽取失败: {proc.stderr}) return audio_path这里的重点是把 stderr 捕获出来而不是扔到终端否则你批量处理 100 个文件时哪个文件失败了根本不知道。-y表示覆盖输出文件而不询问这在循环处理时能避免进程挂起等待输入。3.3 Python 里整合音频提取和转录设计一个转换器类有了音频抽取和 Whisper 转录这两个能力就可以把它们组合成一个转换器类。我设计这个类时要求它既能处理单个文件也能处理整个目录既能输出纯文本也能输出带时间戳的 srt 字幕。import whisper from pathlib import Path class AudioVideoTranscriber: def __init__(self, model_namesmall, devicecpu): self.model whisper.load_model(model_name, devicedevice) def transcribe_file(self, video_path, output_diroutput): video_path Path(video_path) output_dir Path(output_dir) output_dir.mkdir(exist_okTrue) # 抽音频 audio_path output_dir / (video_path.stem .wav) extract_audio(str(video_path), str(audio_path)) # 转文字 result self.model.transcribe(str(audio_path), languagezh, fp16False) # 写文本文件 (output_dir / (video_path.stem .txt)).write_text( result[text], encodingutf-8 ) # 写 srt 字幕 self._write_srt(result[segments], output_dir / (video_path.stem .srt)) return result[text] def _write_srt(self, segments, srt_path): lines [] for i, seg in enumerate(segments, start1): start self._format_srt_time(seg[start]) end self._format_srt_time(seg[end]) lines.append(f{i}\n{start} -- {end}\n{seg[text].strip()}\n) srt_path.write_text(\n.join(lines), encodingutf-8) def _format_srt_time(self, seconds): hours int(seconds // 3600) minutes int((seconds % 3600) // 60) secs int(seconds % 60) millis int((seconds - int(seconds)) * 1000) return f{hours:02}:{minutes:02}:{secs:02},{millis:03}这个类的设计思路是构造函数只负责加载模型避免每个文件重复加载transcribe_file负责整个流程编排辅助方法_write_srt把 Whisper 返回的 segments 转成标准字幕格式。segments是 Whisper 里带时间边界的文本块每个元素有start、end、text三个关键字段。_format_srt_time把秒数转换成小时:分钟:秒,毫秒格式因为 srt 的时间戳要求毫秒用逗号分隔不是小数点这是字幕软件读取时最容易出错的地方。实际使用中这个类还能继续扩展比如加入language参数、task参数转写或翻译但核心的编排逻辑已经足够撑起一个可用的转换器。4. 把源码拆开看一个可扩展的音频视频转文字转换器应该有哪些模块4.1 模块划分文件发现、音频提取、转录、结果导出上一章的转换器类能工作但如果把源码直接堆在一个文件里后面加需求就会很痛苦。设计源码时我会按职责分成四个模块彼此之间通过函数或类接口调用。最常见做法是文件发现模块递归扫描目录找出所有 mp4、mkv、avi、mov、flv、mp3、wav 等音视频文件并过滤掉已经处理过的。音频提取模块统一用 ffmpeg 把各类视频抽成 16kHz 单声道 wav对音频文件则直接复制或转码。转录模块加载 Whisper 模型对 wav 文件执行transcribe返回原始文本和 segments。结果导出模块把文本写成 txt、srt、vtt或者拼接成 JSON 供后续流程使用。这种拆法的好处是如果你要换掉 Whisper只需要重写转录模块如果你要加人声分离只需在音频提取和转录之间插入一步。很多现成的开源项目就是这个结构只不过它们把命令行参数、日志、批量队列也加了进来复杂度陡增。为了保持源码可读我习惯用dataclass定义中间数据让每个模块的输入输出清晰可见from dataclasses import dataclass from pathlib import Path dataclass class MediaFile: path: Path kind: str # video or audio duration: float property def stem(self): return self.path.stem dataclass class TranscriptResult: text: str segments: list language: strMediaFile负责描述一个待处理文件TranscriptResult负责携带转写结果。这样在编写批量流程时你可以把MediaFile列表先构建好再逐个处理避免在函数调用里用video_path、audio_path、result_text这种散装变量来回传。4.2 关键参数 batch_size 与 language 的实战设置Whisper 的transcribe参数里除了模型名batch_size、language、fp16、condition_on_previous_text这 4 个是最常调错的。我先说batch_size它控制的是模型内部对音频分块并行解码的块数不是一次处理几个文件。默认值是 8但在 CPU 上batch_size8反而会因为内存占用过高而变慢我建议 CPU 设 2 或 4GPU 设 8 或 16。language前面已经说过必须有意识地设置。另一个参数condition_on_previous_text更隐蔽默认是 True意思是模型会参考上一段的文本生成下一段这本来能提升连贯性但在有背景音乐或多人讲话时它会导致“重复生成同一句话”的循环。如果发现转录文本出现大段重复就把这个参数设为 False代价是长音频的上下文连贯性稍微下降。实战中我一般这样设置result self.model.transcribe( audio_path, languagezh, fp16torch.cuda.is_available(), batch_size4 if torch.cuda.is_available() else 2, condition_on_previous_textTrue )注意fp16直接关联torch.cuda.is_available()这比硬编码True或者False更可靠因为同一套源码在不同机器上跑不需要改代码。batch_size的赋值逻辑同理用设备类型自动决定避免在一台 8G 显存的卡上因为 batch_size 过大而 OOM。4.3 输出格式选择txt/srt/vtt 与时间戳对齐转换器最终要给用户什么格式会直接影响一个项目能不能被接受。纯 txt 最简单但丢了时间信息无法对应到视频画面srt 是各大播放器和剪辑软件通用的字幕格式vtt 是 Web 字幕格式用于网页播放器。设计源码时我会做一个导出函数支持这三种格式并用一个参数控制def export_result(result: TranscriptResult, output_stem: str, fmt: str srt): if fmt txt: text result.text elif fmt srt: text segments_to_srt(result.segments) elif fmt vtt: text segments_to_vtt(result.segments) else: raise ValueError(f不支持的输出格式: {fmt}) Path(output_stem . fmt).write_text(text, encodingutf-8) return textsegments_to_srt和segments_to_vtt的区别在于时间戳格式srt 用逗号分隔毫秒vtt 用小数点并且 vtt 第一行必须写WEBVTT。如果不注意这个细节生成的文件在播放器里会直接报格式错误。我在做字幕导出时常常需要处理时间戳对齐问题Whisper 返回的 segment 时间戳是基于整段音频的绝对时间但如果你的视频在抽音频时做了裁剪必须对时间做偏移校正。这个容易踩坑我在下一章细说。5. 避坑与常见问题Whisper 转录翻车的 5 条血泪经验5.1 中文识别出现繁体字或夹杂英文现象一段普通话录音转出来的文字一半是简体一半是繁体偶尔还蹦出几个英文字母。原因分析Whisper 的训练数据里中文语料繁简混杂且语言检测阶段可能把粤语或文语混合当成目标。解决方法在transcribe中固定languagezh并将initial_prompt设置成“以下是简体中文普通话的转录文本”能明显减少繁体和英文残留result model.transcribe( audio_path, languagezh, initial_prompt以下是简体中文普通话的转录文本。 )这个技巧属于“玄学”派别但对 OpenAI 的多语言模型确实起效原理是给解码器一个先验语境让它偏向简体中文输出。5.2 GPU 显存足够却报 CUDA out of memory现象显卡是 12G 显存加载 large 模型也成功但一跑transcribe就报CUDA out of memory。原因分析显存足够加载模型但batch_size或fp16设置不当推理时的激活值把剩余显存吃满了。解决方法先设置torch.cuda.empty_cache()再把batch_size降到 4 或 2并开启fp16True减少一半显存占用。如果仍然 OOM检查是否有其他进程占用显存用nvidia-smi看一眼。5.3 视频音轨是立体声导致转录结果重复现象同一个视频转出来的文字每一句话都出现两遍像是“你好你好”。原因分析Whisper 内部对立体声音频没有自动合并左右声道相同语音被识别成了两条。解决方法在 ffmpeg 抽取音频时务必加上-ac 1强制转成单声道。如果你的流程已经生成 wav可以重新执行抽取命令覆盖原文件而不是在代码里做声道转换因为-ac 1在 ffmpeg 里是最可靠的处理。5.4 transcribe 直接把整个视频读入内存导致卡死现象处理一个 2 小时电影程序在读取阶段就占用十几 G 内存然后系统变得卡顿。原因分析Whisper 官方代码会用ffmpeg把音频加载到内存如果采样率高、声道多内存占用会超出预期。解决方法抽音频时输出 wav 到磁盘并检查文件大小一个 2 小时的 16kHz 单声道 wav 大约 230MB如果你抽出来的 wav 有 1GB说明采样率或声道没设置对。另外Whisper 对长音频会做内部切片但切片前的完整音频已常驻内存所以不要用超大内存的机器做这个32G 内存足够 4 小时音频。5.5 长音频转录半小时后进程被杀现象处理一个 90 分钟的播客跑到一半进程消失没有任何报错。原因分析常见于 OOM killer 或其他进程抢占资源也可能是transcribe回调函数里累积了太多段数据。解决方法给 Python 进程设置ulimit或malloc调试不现实最好的是把长音频切分成 20-30 分钟的片段逐段转录后合并 segments。Whisper 生态里有人写了long_form支持但官方transcribe其实内部已经分段所以进程被杀大概率是其他问题。用dmesg | tail -20查看内核日志就能看到是不是被 OOM killer 杀的。我碰到的案例是系统开了 docker 导致内存碎片化关掉其他服务后问题消失。6. 让转换器更实用批量任务、断点续传与模型热切换到了这一步单文件的转换器已经能跑但要应对真实工作流还得补三个功能批量处理、断点续传、按需换模型。批量处理可以用Path.glob或os.walk收集文件然后用concurrent.futures.ThreadPoolExecutor并行处理。这里要注意Whisper 模型不是线程安全的每个线程需要独立的模型实例显存会成倍上升。我通常用进程池而不是线程池每个进程只加载一次模型处理完一个文件后再取下一个。断点续传是批量任务的后悔药。实现起来不复杂在每次处理完一个文件后把它的文件名写进一个done.json下次处理前先读取这个文件跳过已完成项。设计源码时我会把done.json的读写封装成一个小小的缓存器避免每次循环都打开文件import json class TaskCache: def __init__(self, cache_path): self.path cache_path self.done set() if self.path.exists(): self.done set(json.loads(self.path.read_text(encodingutf-8))) def mark(self, key): self.done.add(key) self.path.write_text( json.dumps(list(self.done), ensure_asciiFalse, indent2), encodingutf-8 ) def is_done(self, key): return key in self.doneTaskCache的核心价值在于崩溃恢复。批量转写十几个视频时如果进度到 80% 断电重启后能直接从 80% 继续而不是从头再来。注意json.dumps里要加ensure_asciiFalse否则文件名如果是中文会被转成\uXXXX不影响功能但可读性很差。模型热切换则解决另一种需求不同视频时长不同短音频用 small 足够长音频或复杂语料用 large 更稳。在源码里设计一个函数根据文件时长动态选择模型名然后用getattr或者字典维护模型实例池MODEL_POOL { short: base, medium: small, long: medium, } def pick_model(duration_seconds): if duration_seconds 300: return MODEL_POOL[short] elif duration_seconds 1800: return MODEL_POOL[medium] else: return MODEL_POOL[long]切换模型时我之前犯过一个错每次都重新load_model结果一个批次的视频里反复加载了 20 次模型白白浪费大量时间。后来改成了实例池按需加载并缓存同一个模型只加载一次。这就是“模型热切换”的实际含义不是真的在推理中途换而是在批次间按需懒加载。这三个功能合在一起你的源码才算真正从“演示脚本”进化成“工具”。我每次把别人的视频转成带字幕的文档时都会先用 small 跑一个 1 分钟片段确认语速、背景噪音、说话人数都没问题再铺到全量数据上。这个习惯救了我很多次因为 Whisper 对口语的识别表现有时候真的只能靠经验去试。希望这套设计思路能帮你在自己的机器上少走点弯路把音频视频转文字这件小事做踏实。本文还有配套的精品资源点击获取
返回列表