公司动态

用Whisper打造本地音视频摘要工具:从转写到生成TLDR

📅 2026/8/27 6:31:34
用Whisper打造本地音视频摘要工具:从转写到生成TLDR
音视频内容越来越长播客一期动辄一个小时技术分享录屏往往也超出一次性看完的耐心。要判断一段内容值不值得投入时间常见做法要么快进试听要么先找字幕。audio-tldr 这类工具提供的是第三种思路把视频或播客文件保存在本地用 Whisper 完成语音转写得到文字稿再自动生成一段“太长不看”式的短摘要。这样既能快速判断内容价值也能把播客、访谈、课程沉淀成可检索的文本笔记。这类工具最大的价值在于本地运行。音频文件不会上传到云端识别服务对访谈、内部培训、未发布内容等隐私敏感场景更友好同时断网可用也没有按分钟计费的 API 成本。下面先说明这套链路为什么成立再完成一个最小可运行版本最后补充常见陷阱、排查路径和扩展方向。1. 先理解 audio-tldr 的处理链路和 Whisper 的角色1.1 本地摘要工具到底做了哪几件事audio-tldr 这个名字可以拆成 audio 加 tldrtoo long; didnt read。它解决的问题是把“听完整段内容”这个高成本动作降级成“读一段几百字的摘要”。从工程角度看完整流程分为四步获取输入本地文件常见格式包括 mp4、mp3、m4a、wav也可以来自录屏、播客下载或会议录音。抽取音频如果输入是视频先用工具剥离视频轨只保留声音并统一成适合语音识别的编码。语音转写把音频内容变成文字稿这是整条链路的精度瓶颈。文本摘要把长篇文字稿压缩成核心观点、关键细节和结论。第 3 步决定了文字稿质量第 4 步决定了摘要是否可用。两者的技术路线不同不能混为一谈。1.2 Whisper 是什么为什么适合这个场景Whisper 是 OpenAI 开源的语音识别模型输入音频后输出文字稿并且能给出每句话对应的时间戳。它基于 Transformer 结构在大量多语种音频上训练对背景噪声和多语言内容有较好的适应能力原生仓库以 MIT 协议开放可以本地推理。需要澄清一点Whisper 只负责转写不负责摘要。它输出的是一段文字而不是“总结观点”。audio-tldr 最后的摘要由下游的文本摘要阶段单独完成。最容易误解的地方就在这里以为装了 Whisper 就自动有了摘要能力实际上摘要还要另外写逻辑或接本地大模型。1.3 为什么不在线调用 ASR 服务很多项目直接调用云端语音识别接口速度快、精度高但代价也很明显。把在线方案和本地 Whisper 放在一起对比选型会更清楚维度在线 ASR 服务本地 Whisper网络依赖必须联网模型下载后可离线隐私音频上传到服务商文件不出本机费用按时长或调用量计费主要花费在硬件和电费部署复杂度低拿 API Key 就能用需要安装 Python、ffmpeg、模型延迟受网络影响取决于 CPU/GPU 资源模型可控性不能替换模型可以换 tiny 到 large 各尺寸如果你的场景是长期处理内部会议录音、未发布课程或敏感访谈本地方案在隐私和数据安全上有明显优势。如果只是偶尔处理几段公开内容并且要求最高识别精度在线服务也是一个合理选项。2. 环境准备版本、硬件和依赖2.1 先确认环境要求audio-tldr 依赖三部分Python 运行环境、ffmpeg 音频处理工具、Whisper 模型推理。Python 版本以 openai-whisper 当前要求为准通常建议 3.9 或更高GPU 不是必需但会影响大模型的处理速度。组件说明操作系统Linux、macOS、Windows 均可Python建议 3.9 以上使用虚拟环境隔离依赖ffmpeg负责抽取音轨、转码必须有PyTorch安装 openai-whisper 时会自动带上来GPU可选处理 large 模型或长音频时明显更快学习环境中可以用 CPU 跑 tiny 或 base 模型完成链路验证生产环境再根据音频时长和精度要求决定是否上 GPU。2.2 安装 ffmpeg 和 Whisper先创建虚拟环境避免依赖污染系统 Pythonpython3 -m venv .venv source .venv/bin/activate安装 openai-whisperpip install -U openai-whisper接下来安装 ffmpeg。不同系统命令不同# Ubuntu / Debian sudo apt update sudo apt install -y ffmpeg # macOS brew install ffmpegWindows 上可以从 FFmpeg 官网或包管理器安装安装后把可执行文件目录加入 PATH并重开终端让环境变量生效。2.3 Whisper 模型如何选型Whisper 官方提供了多个尺寸的模型选型直接影响识别速度和中文准确率。表格里是常见选型参考实际以当前版本为准模型参数规模运行资源速度适用场景tiny约 39MCPU 可跑最快快速验证链路base约 74MCPU 可跑快短音频、英文测试small约 244M需要一定内存中等中文识别最低建议medium约 769M建议 GPU慢精度优先的内容large约 1550M高显存或 CPU 长时间跑最慢正式生产精度要求高对于中文播客和视频优先从 small 起步。tiny 和 base 在中文长句上的错字率会明显偏高。如果只是先验证“能不能跑通”用 tiny 比用 large 划算得多。2.4 安装后先做环境自检在写代码前先确认两个关键命令可用ffmpeg -version再验证 Whisper 模块能否导入并加载模型python -c import whisper; whisper.load_model(tiny); print(whisper ok)第一次执行 load_model 会从网络下载模型权重之后会缓存在本地目录。这里要注意所谓“本地运行”不代表永远不联网首次下载模型仍然需要网络。3. 编写 audio-tldr 核心代码3.1 项目目录结构一个单文件版本就足够跑通最小闭环。为了让后续扩展更清晰可以按下面结构组织audio-tldr/ ├── requirements.txt ├── audio_tldr.py └── output/ └── result.jsonrequirements.txt 内容openai-whisper requestsrequests 只在“摘要模式使用本地大模型”时需要。如果只走抽取式摘要可以暂时不装。3.2 第一步用 ffmpeg 抽取音频Whisper 内部会自行处理音频重采样但传入一个规范的 16kHz、单声道 wav 文件能减少中间环节的兼容问题。ffmpeg 抽取音频的代码可以封装成一个函数import subprocess from pathlib import Path def extract_audio(input_path, output_path, sample_rate16000): 用 ffmpeg 把音视频文件转成 16kHz 单声道 wav。 cmd [ ffmpeg, -y, -i, str(input_path), -vn, -ac, 1, -ar, str(sample_rate), str(output_path), ] subprocess.run(cmd, checkTrue, capture_outputTrue)参数说明-vn表示去掉视频轨只保留音频。-ac 1把声道合并成单声道语音识别不需要立体声。-ar 16000把采样率统一到 16kHz这也是 Whisper 偏好的输入规格。3.3 第二步本地转写得到文字稿转写阶段加载 Whisper 模型并调用 transcribedef transcribe(audio_path, model_namebase, languageNone): import whisper model whisper.load_model(model_name) result model.transcribe( str(audio_path), languagelanguage, fp16False, verboseFalse, ) return resultfp16False在 CPU 环境下是必要设置。如果保持默认的 fp16部分 CPU 平台会直接报错或产生无效结果。返回的 result 是字典常用字段包括text、segments、language和duration。注意不要只验证程序能启动还要验证转写文本是否和音频内容吻合。模型加载成功不代表识别结果正确。3.4 第三步生成摘要摘要有两种实现路径抽取式摘要和本地大模型生成式摘要。抽取式不依赖额外服务速度最快本地大模型质量更好但需要另装推理服务。抽取式摘要按句子的位置和长度打分选出相对重要的句子def extractive_summary(text, max_sentences5): import re sentences re.split(r(?[。!?])\s*, text.strip()) sentences [s.strip() for s in sentences if len(s.strip()) 10] if not sentences: return text[:200] scored [] for idx, sentence in enumerate(sentences): position_score 1.0 if idx len(sentences) * 0.3 else 0.5 length_score min(len(sentence), 200) / 200.0 scored.append((position_score length_score, idx, sentence)) scored.sort(reverseTrue) selected sorted(scored[:max_sentences], keylambda x: x[1]) return \n.join(item[2] for item in selected)这种方式实现简单但它只是“选句”不是“总结”。如果希望输出更有逻辑、更像人写的摘要可以调用本地 Ollamadef llm_summary(text, modelqwen2.5:7b, max_input_chars12000): import requests prompt ( 你是音视频内容摘要助手。请基于下面的文字稿输出结构化摘要\n 1. 核心观点\n 2. 关键细节\n 3. 结论或行动建议\n 全文控制在 300 字以内使用中文。\n\n f文字稿\n{text[:max_input_chars]} ) resp requests.post( http://localhost:11434/api/generate, json{model: model, prompt: prompt, stream: False}, timeout300, ) resp.raise_for_status() return resp.json()[response]调用前需要先安装 Ollama 并下载模型例如ollama pull qwen2.5:7b。这段代码把文字稿截断到 12000 字符再送入模型避免超出上下文窗口。3.5 第四步主流程串联把上面几个函数放进同一个文件再加上命令行解析和结果输出就是一个完整的最小工具import argparse import json import shutil import subprocess import tempfile import time from pathlib import Path def extract_audio(input_path, output_path, sample_rate16000): cmd [ ffmpeg, -y, -i, str(input_path), -vn, -ac, 1, -ar, str(sample_rate), str(output_path), ] subprocess.run(cmd, checkTrue, capture_outputTrue) def transcribe(audio_path, model_namebase, languageNone): import whisper model whisper.load_model(model_name) result model.transcribe( str(audio_path), languagelanguage, fp16False, verboseFalse, ) return result def extractive_summary(text, max_sentences5): import re sentences re.split(r(?[。!?])\s*, text.strip()) sentences [s.strip() for s in sentences if len(s.strip()) 10] if not sentences: return text[:200] scored [] for idx, sentence in enumerate(sentences): position_score 1.0 if idx len(sentences) * 0.3 else 0.5 length_score min(len(sentence), 200) / 200.0 scored.append((position_score length_score, idx, sentence)) scored.sort(reverseTrue) selected sorted(scored[:max_sentences], keylambda x: x[1]) return \n.join(item[2] for item in selected) def llm_summary(text, modelqwen2.5:7b, max_input_chars12000): import requests prompt ( 你是音视频内容摘要助手。请基于下面的文字稿输出结构化摘要\n 1. 核心观点\n 2. 关键细节\n 3. 结论或行动建议\n 全文控制在 300 字以内使用中文。\n\n f文字稿\n{text[:max_input_chars]} ) resp requests.post( http://localhost:11434/api/generate, json{model: model, prompt: prompt, stream: False}, timeout300, ) resp.raise_for_status() return resp.json()[response] def main(): parser argparse.ArgumentParser(descriptionaudio-tldr: 本地音视频摘要工具) parser.add_argument(input, help本地音视频文件路径) parser.add_argument(-m, --model, defaultbase, helpWhisper 模型名) parser.add_argument(-l, --language, defaultNone, help音频语言代码如 zh 或 en) parser.add_argument(--summary-mode, defaultextractive, choices[extractive, llm]) parser.add_argument(--llm-model, defaultqwen2.5:7b, help本地 Ollama 模型名) parser.add_argument(--output, defaultNone, help结果 JSON 输出路径) parser.add_argument(--keep-audio, actionstore_true, help保留中间 wav 文件) args parser.parse_args() input_path Path(args.input) if not input_path.exists(): raise FileNotFoundError(f输入文件不存在: {input_path}) start time.time() with tempfile.TemporaryDirectory(prefixaudio-tldr-) as tmp_dir: wav_path Path(tmp_dir) / audio.wav print(f1/4 正在抽取音频: {input_path.name}) extract_audio(input_path, wav_path) if args.keep_audio: keep_path Path(audio_debug.wav) shutil.copy2(wav_path, keep_path) print(f调试音频已保留: {keep_path}) print(f2/4 正在转写模型: {args.model}) result transcribe(wav_path, args.model, args.language) text (result.get(text) or ).strip() print(3/4 正在生成摘要) if not text: summary 未识别到有效文字请检查音频质量和语言参数。 elif args.summary_mode llm: summary llm_summary(text, args.llm_model) else: summary extractive_summary(text) elapsed time.time() - start print(4/4 处理完成) print( * 60) print(摘要) print( * 60) print(summary) print() print( * 60) print(转写全文) print( * 60) print(text) print() print(f音频时长: {result.get(duration, 0):.1f} 秒) print(f处理耗时: {elapsed:.1f} 秒) if args.output: output_path Path(args.output) output_path.parent.mkdir(parentsTrue, exist_okTrue) output_path.write_text( json.dumps({ summary: summary, transcript: text, language: result.get(language), audio_duration: result.get(duration), processing_seconds: round(elapsed, 2), }, ensure_asciiFalse, indent2), encodingutf-8, ) print(f结果已保存: {output_path}) if __name__ __main__: main()临时音频放在tempfile.TemporaryDirectory中处理结束会自动清理。如果需要保留 wav 做调试使用--keep-audio。4. 运行验证与效果评估4.1 准备一段测试音频最简单的测试音频可以自己生成。在 Linux 上可以用 espeak 生成英文试听片段espeak -v en-us -s 160 -w sample.wav Audio tldr turns long videos into short summaries with whisper, running locally on your machine.更接近真实场景的做法是准备一段 30 秒到 1 分钟的中文播客片段导出成 mp3 或 m4a。音频太短无法检验长句转写质量太长又会拖慢首次验证速度。4.2 命令行用法和预期输出先跑抽取式摘要模式python audio_tldr.py sample.wav -m base -l en --output result.json如果音频是中文python audio_tldr.py podcast.mp3 -m small -l zh --output result.json下面是一次正常运行的输出示例1/4 正在抽取音频: podcast.mp3 2/4 正在转写模型: small 3/4 正在生成摘要 4/4 处理完成 摘要 本文讨论的是本地音视频摘要工具。核心思路是先使用 Whisper 完成语音转写再通过文本摘要生成简短结论。工具支持中文音频默认全程在本机处理适合隐私敏感场景。 转写全文 今天我们来讨论一个本地运行的音视频摘要工具。它先把视频或者播客转成文字然后再用摘要算法提取核心观点。整个过程不需要上传到云端服务……process 最后两行会输出音频时长和处理耗时。第一次运行 large 模型时耗时可能让很多人误以为程序卡死实际上是在加载模型。4.3 摘要质量怎么判断摘要质量不能只看“有没有输出”。建议从三个角度检查核心观点是否覆盖听完原内容后摘要是否包含了最重要的结论。关键细节是否保留人名、数字、日期、工具名是否还在。是否有原文不存在的推测抽取式摘要不会编造但顺序可能混乱大模型摘要可能自行补全必须核对。如果发现抽取式摘要只是把开头几句话拼在一起说明原音频的叙述结构比较发散这时可以换用生成式摘要或者调大 max_sentences。4.4 资源消耗与耗时参考不同模型在同一段 10 分钟音频上的表现差异很大。下面是 CPU 环境下常见的大致感受实际取决于机器配置模型CPU 处理 10 分钟音频显存占用GPUtiny半分钟到 1 分钟可忽略base1 到 3 分钟约 1GB 上下small5 到 10 分钟约 2GB 级别medium20 分钟以上建议 5GB 以上large较久建议 GPU建议 10GB 级别这些数字只是参考不要在文档里当作绝对性能承诺。真正选型时处理两段内容记录耗时和准确率即可。5. 关键参数与长音频处理5.1 transcribe 关键参数速查Whisper 的 transcribe 参数很多日常最常用的集中在这里参数默认作用建议languageNone指定语言None 表示自动