150行Python实现视频转逐字稿:Whisper本地离线方案全拆解

发布时间:2026/7/13 14:57:24
150行Python实现视频转逐字稿:Whisper本地离线方案全拆解 项目概述Video2Txt 是一个基于 Whisper large-v3-turbo 的本地视频转文字工具仅 138 行 Python 代码。核心流程分三步拆——用 FFmpeg 把视频中的音频轨道剥离按 20 分钟一段切分成 WAV 片段听——用 OpenAI 的 Whisper 模型large-v3-turbo、CTranslate2 格式逐段做语音识别拼——把所有片段的识别结果按全局时间戳拼回完整文本输出带时间戳版和纯文本版两份文件整个过程不联网、不上传任何数据。模型量化后仅占约 1.5GB 磁盘空间运行时内存消耗在 2-4GB 之间纯 CPU 可跑。对于需要处理会议录音、访谈视频、课程录像等场景这套方案兼顾了隐私安全和零成本两个硬需求。技术背景为什么选这套技术栈做本地语音识别可选的方案其实不少但每种都有自己的短板方案优点短板Whisper.cpp ggml 模型极致轻量C 推理快中文识别准确率一般模型生态不如 CTranslate2 丰富FunASR阿里达摩院中文效果优秀自带 VAD 和标点依赖较多安装过程容易踩环境坑OpenAI API / 讯飞等云服务开箱即用无需本地算力按量计费、网络依赖、数据上传合规风险faster-whisper CTranslate2准确率高、量化灵活、API 简洁纯 CPU 推理速度不如 GPU但 int8 量化后差距可接受最终选型落在faster-whisperwhisper-large-v3-turbo上理由很明确large-v3-turbo是 Whisper 系列中性价比最高的模型体积比 large-v3 小一半~1.5GB vs ~3GB推理速度快 8 倍字错率WER差异不到 1%。对中文语音尤其是有口音的会议场景turbo 版本比 medium 版本强出一个档次。faster-whisper底层用的是 CTranslate2 推理引擎原生支持 int8 量化8 线程下 RTF实时因子能做到 0.3 左右——1 分钟音频约 20 秒处理完。CTranslate2 格式模型可以直接从 HuggingFace 下载后本地加载配合local_files_onlyTrue彻底断网运行适配内网环境。项目结构与配置区整个项目是单文件架构main.py138 行。打开第一眼看到的是顶部的配置区所有可调参数集中暴露VIDEO_PATH rD:\video.mp4 LOCAL_MODEL_PATH rD:\Models\whisper-large-v3-turbo SEGMENT_MINUTES 20 COMPUTE_TYPE int8 CPU_THREADS 8 LANGUAGE zh BEAM_SIZE 3参数逐一拆解VIDEO_PATH指向目标视频文件。r前缀防止 Windows 路径中反斜杠被当作转义字符——\n、\t这类坑在 Windows 开发中屡见不鲜。LOCAL_MODEL_PATH整个项目的灵魂参数。faster-whisper支持直接传入本地 CTranslate2 格式模型的目录路径而不走 HuggingFace 远程下载。模型可去 HuggingFace 搜索deepdml/faster-whisper-large-v3-turbo-ct2下载后目录内需包含model.bin可能有分片、config.json、tokenizer.json、preprocessor_config.json。整体约 1.5GB放到本地任意路径即可。SEGMENT_MINUTES 20每段切 20 分钟是经过多轮测试确定的甜点值。Whisper 的注意力窗口有限过长音频会导致尾部识别质量衰减切太碎比如 10 分钟则会增加 FFmpeg 切分和模型加载上下文的开销。20 分钟在两者间取得了平衡——既保证单段质量又不会把一小时视频拆出六段以上。另一个隐含好处是容错某一段崩了只需重跑这一段不用从头来。COMPUTE_TYPE int8faster-whisper的量化精度选项。CPU 上只能用int8——float16和int8_float16是 CUDA GPU 专属。int8 将模型权重从 32 位浮点压缩到 8 位整数内存占用砍到四分之一中文识别的精度损失几乎不可感知。实测 32GB 内存环境中模型运行时仅占不到 2GB。CPU_THREADS 8设成物理核心数不是逻辑线程数。以 i7-137008P 8E为例设为 8 会让faster-whisper把推理任务绑定到性能核上。超线程对 Whisper 推理帮助有限多出来的逻辑核心反而可能因线程切换降低效率。LANGUAGE zh提前指定中文可以跳过 Whisper 内置的语种检测环节省几秒钟。纯英文场景改en中英混杂设None走自动检测。BEAM_SIZE 3束搜索宽度——解码时保留的候选路径数。值越大越准但越慢。CPU 上不建议超过 5实测设为 5 时速度接近翻倍但准确率提升微乎其微。3 是平衡点。核心函数逐段拆解第一关extract_and_split()——音频提取与切分def extract_and_split(video_path: str, output_dir: str, segment_minutes: int) - list[str]: os.makedirs(output_dir, exist_okTrue) pattern os.path.join(output_dir, seg_%04d.wav) segment_seconds segment_minutes * 60 cmd [ ffmpeg, -y, -i, video_path, -vn, -acodec, pcm_s16le, -ar, 16000, -ac, 1, -f, segment, -segment_time, str(segment_seconds), -reset_timestamps, 1, pattern ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: raise RuntimeError(fffmpeg失败:\n{result.stderr}) segments sorted(glob.glob(pattern.replace(%04d, *))) return segments这个函数的设计思路是「一步到位」用 FFmpeg 一条命令同步完成「提取音频」和「切分片段」免去先提取完整 WAV 再二次切割的额外 IO。FFmpeg 参数逐行解读-vn丢弃视频流只保留音频轨。画面数据是毫无价值的负载早点扔掉省 CPU 和磁盘吞吐。-acodec pcm_s16le输出为 PCM 16 位有符号整数小端序——即未压缩的原始音频。Whisper 不接受 MP3、AAC 等压缩格式必须是 RAW/WAV。-ar 16000重采样到 16kHz。Whisper 的输入采样率就是 16kHz在 FFmpeg 层提前完成降采样比让模型内部处理高效得多。原始视频音频通常 44.1kHz 或 48kHz降下来数据量减少约三分之二。-ac 1合并为单声道。立体声对语音识别没有额外信息量双声道内容一样并成单声道再省一半数据。-f segmentFFmpeg 内置的切片 muxer按-segment_time自动分割输出文件。-reset_timestamps 1让每段内部时间戳归零方便后续按段独立处理。-y静默覆盖已有同名文件避免重复运行时的交互卡顿。切分完成后glob.glob(pattern.replace(%04d, *))把 FFmpeg 输出的seg_0001.wav、seg_0002.wav等全部捞出来sorted保证顺序。第二关transcribe_segments()——本地模型逐段转录这是项目的核心也是参数决策最密集的地方def transcribe_segments(segment_files: list[str], output_dir: str) - list[dict]: if not os.path.isdir(LOCAL_MODEL_PATH): raise FileNotFoundError( f模型目录不存在: {LOCAL_MODEL_PATH}\n f请确认目录内包含 model.bin、config.json、tokenizer.json 等文件 ) model WhisperModel( LOCAL_MODEL_PATH, devicecpu, compute_typeCOMPUTE_TYPE, cpu_threadsCPU_THREADS, num_workers1, local_files_onlyTrue, )三个关键决策LOCAL_MODEL_PATH传本地路径faster-whisper的隐藏用法。官方文档通常示范传入模型名称如large-v3-turbo库会自动从 HuggingFace 拉取。但传入本地 CTranslate2 格式模型的目录路径同样有效配合local_files_onlyTrue完全隔离网络。num_workers1数据加载并行度。CPU 推理时推理线程本身已是瓶颈多个 data worker 只会抢 CPU 时间片。设为 1 最稳。local_files_onlyTrue核心红线。不设这个 flag库可能在启动时联网检查模型更新哪怕你已经传了本地路径。加上后彻底阻止一切网络请求。逐段转录循环for idx, seg_file in enumerate(segment_files): global_offset idx * SEGMENT_MINUTES * 60 segments, info model.transcribe( seg_file, languageLANGUAGE, beam_sizeBEAM_SIZE, vad_filterTrue, vad_parametersdict(min_silence_duration_ms600, speech_pad_ms400), condition_on_previous_textTrue, word_timestampsFalse, temperature0.0, )global_offset因为每段音频被独立切分后时间戳都从 0 开始最终输出需要全局偏移量对齐回原始视频时间轴。第 N 段偏移量为N × 20 × 60秒在结果组装阶段统一加上。转录参数说明vad_filterTrue开启语音活动检测Voice Activity Detection自动跳过静音段落。对于会议录音意义重大——翻纸、喝水、长时间的沉默直接跳过不浪费算力。vad_parametersmin_silence_duration_ms600表示连续 600ms 的静音才判定为段落间隙短于这个的不切避免正常说话中的换气停顿被误判。speech_pad_ms400在有声段落首尾各加 400ms 缓冲防止开头结尾的字被截断。condition_on_previous_textTrue识别当前段时携带前文的文本上下文。对中文识别效果立竿见影——同音字选择严重依赖语义约束。例如前文提到「银行利率」后文「提升两个百分点」就能被正确识别而不会被误听成「版分点」。word_timestampsFalse不要词级时间戳只要句子级。词级对中文分词精度要求高实测耗时增加但收益不大。temperature0.0贪婪解码每步选最高概率 token。语音识别追求的是精准复刻而非创造性输出温度归零是最佳选择。结果组装与实时写盘seg_results [] for seg in segments: seg_results.append({ global_start: global_offset seg.start, global_end: global_offset seg.end, text: seg.text.strip(), }) # 实时写入防丢失 tmp_path os.path.join(output_dir, f{seg_name}.txt) with open(tmp_path, w, encodingutf-8) as f: for r in seg_results: f.write(f[{_fmt_ts(r[global_start])}] {r[text]}\n)每处理完一段立即写盘而非等全部跑完再统一写。这个机制在面对长视频时价值巨大——假设第 18 段崩溃前 17 段的结果已经安全落盘重跑只需从第 18 段续上。第三关merge_results()——文本合并输出def merge_results(results: list[dict], output_path: str): with open(output_path, w, encodingutf-8) as f: for r in results: f.write(f[{_fmt_ts(r[global_start])}] {r[text]}\n) pure_path output_path.replace(.txt, _pure.txt) with open(pure_path, w, encodingutf-8) as f: f.write(\n.join(r[text] for r in results))两份输出各司其职xxx_transcript.txt带[HH:MM:SS]时间戳的完整版适合需要精确定位原文位置的场景写纪要时回溯上下文xxx_transcript_pure.txt纯文本适合直接喂给大语言模型做摘要、要点提取、结构化整理辅助函数_fmt_ts()def _fmt_ts(seconds: float) - str: h int(seconds // 3600) m int((seconds % 3600) // 60) s int(seconds % 60) return f{h:02d}:{m:02d}:{s:02d}把浮点秒数转成HH:MM:SS格式。两个小时视频的时间戳就能显示成02:03:15而不是7395.2。实现简洁直白没有额外依赖。主流程串联if __name__ __main__: video Path(VIDEO_PATH) work_dir str(video.parent / f{video.stem}_asr_work) final_output str(video.parent / f{video.stem}_transcript.txt) seg_files extract_and_split(str(video), work_dir, SEGMENT_MINUTES) results transcribe_segments(seg_files, work_dir) merge_results(results, final_output)工作目录和输出文件名自动从视频文件名派生视频叫客户会议.mp4工作目录就是客户会议_asr_work/输出文件就是客户会议_transcript.txt。用户只需改好VIDEO_PATH。实测性能在两小时会议视频中文、带轻度口音的测试场景下指标数据视频时长120 分钟硬件i7-13700 (8P8E)、32GB DDR5切分耗时~45 秒FFmpeg 提取切 6 段转录总耗时~38 分钟平均 RTF0.32字错率CER5%-8%模型内存占用~1.8GBRTF 0.32 意味着 1 分钟音频约 19 秒处理完。纯 CPU 方案能做到这个速度主要归功于 int8 量化和 turbo 模型的轻量化设计。字错率 5%-8% 对中文会议场景属于可用范围——专业术语如「OCPC」「ROI」偶尔出错整体语义完整可读。输出效果示例带时间戳版[00:00:02] 好那我们现在开始今天的会议 [00:00:07] 主要讨论一下Q2的投放策略调整 [00:00:13] 先请张总给我们同步一下最新的数据 ...纯文本版直接就是干净的对话内容可直接粘贴进大模型做后续处理。常见踩坑与解决方案模型下载失败HuggingFace 在国内的访问稳定性堪忧。建议走镜像站hf-mirror.com或使用hfd.sh脚本下载。务必确认目录内文件齐全model.bin可能有分片如model.bin-00001-of-00002、config.json、tokenizer.json、preprocessor_config.json缺任何一个都会加载失败。FFmpeg 不在 PATH 中Windows 安装 FFmpeg 后需手动添加到系统环境变量并重启终端CMD 或 PowerShell才能让subprocess.run找到。验证方式终端输入ffmpeg -version有输出版本号即正常。int8 量化兼容性问题较老的 CPU 如果不支持 AVX2 指令集faster-whisper的 int8 量化可能报错或性能暴跌。此时可尝试将CPU_THREADS降到 4有时瓶颈在内存带宽而非算力。VAD 误杀有效语音min_silence_duration_ms600这个值是反复调参的结果。设太短200ms说话中的正常换气会被当成段落间隙反复切割设太长1000ms超过一秒的思考停顿会被当成静音整段跳过。600ms 适配中文语速——既不会把停顿切碎也不会漏掉内容。内存不足faster-whisper加载 large-v3-turbo 的 int8 版约消耗 1.5-2GB8 线程并发时峰值可能到 4GB。16GB 以上内存无压力8GB 环境建议将CPU_THREADS降到 4必要时关闭其他应用释放内存。扩展方向这 138 行代码的可扩展空间相当充裕英文字幕把LANGUAGE改成encondition_on_previous_text保持True即可处理英文视频批量处理外层套一个目录遍历循环自动处理文件夹内所有 MP4断点续转利用已有逐段写盘机制加一个简单的 JSON 状态文件记录已完成段落编号重跑时跳过GPU 加速有 NVIDIA 显卡且显存 4GB 以上时把devicecpu改成devicecudacompute_type改成float16速度可提升 3-5 倍说话人分离结合pyannote-audio做说话人日志Speaker Diarization在时间戳基础上增加说话人标注小结Video2Txt 不是一个大而全的产品而是聚焦解决一个明确问题在不上传、不付费、不联网的前提下把视频里的语音变成可用的文字。138 行代码、三个函数、七个配置参数每个参数的取值背后都是实测调优的结果。对于有视频转文字需求、又对数据安全和成本敏感的开发者来说这套方案提供了一个直接可用的起点。代码短小精悍改配置即用需要定制的时候也容易下手。项目文件main.py138 行文中已逐段完整解读。运行环境Python 3.10 / FFmpeg / faster-whisper / 本地 Whisper large-v3-turbo 模型CTranslate2 格式