1. 为什么要做本地 AI 字幕工具做视频内容创作或者课程整理的朋友大概率都遇到过这样的场景手上有几十段访谈录音、网课录像或者会议纪要需要把它们转成带时间轴的字幕文件方便后续剪辑、校对或者做二次分发。最初我的做法是找在线工具一个文件一个文件上传转完再手动下载。遇到长视频还要掐着时间分段处理一边担心平台限额一边担心上传的素材会不会被留存。后来换成本地 AI 方案之后这些问题基本都消失了。本文要分享的就是一套完全运行在本地电脑上的批量音视频转文字工具核心功能围绕三点展开批量处理把整个文件夹里的音频、视频一次性交给程序。长音频自动切分不需要手工剪断文件程序会按静音点或其他策略自动分段转写。断点续跑转写到一半断电、报错或手动停止下次启动自动跳过已完成内容。整套方案基于开源语音识别模型实现不需要 GPU 也能跑重点在于工程化封装把模型调用、音频切分、SRT 字幕生成、任务进度记录这几件事整合到一起。读完本文你不仅会得到一个能直接运行的脚本还能理解它背后的设计思路方便后续按自己的需求改造。2. 技术方案与核心概念2.1 音频转文字的常见路径目前主流的语音识别开源方案有 Whisper、faster-whisper、FunASR、Kaldi 等。其中 Whisper 和 faster-whisper 是社区使用最广泛的两类。Whisper 是 OpenAI 开源的语音识别模型支持多语言、带时间戳输出可以直接生成类似 SRT 的段落信息。faster-whisper 是 Whisper 模型的加速推理版本底层使用 CTranslate2 做模型推理在 CPU 上的速度比原版快不少内存占用也更低。对于本地批量处理来说faster-whisper 是更务实的选择。音频切分方面一般有两种思路固定时长切分按 30 秒、60 秒一个片段切分实现简单但可能切断句子导致字幕语义不完整。静音检测切分通过分析音频能量找到相对安静的位置作为切分点能较好地保留完整句子边界。本文采用静音检测为主、固定时长兜底的方式兼顾转写质量和流程稳定性。2.2 SRT 字幕格式SRT 是通用性最强的字幕格式之一几乎所有播放器和剪辑软件都支持。一个 SRT 文件的结构大致如下1 00:00:01,000 -- 00:00:05,000 大家好今天我们来聊一聊字幕工具的实现。 2 00:00:06,500 -- 00:00:10,200 首先需要准备 Python 环境和语音识别模型。其中数字是字幕序号第二行是时间轴第三行开始是字幕文本。时间轴格式必须严格遵循“小时:分钟:秒,毫秒”毫秒部分用逗号分隔。2.3 什么是续跑所谓续跑是指在批量处理过程中记录每个文件、每个切片的状态。当程序异常退出或人为中断时已经处理完成的部分不需要重新转写。实现续跑的关键在于任务状态持久化本文采用 JSON 文件记录任务清单和完成状态每次启动时先读取状态文件再决定哪些任务需要执行。3. 环境准备与项目结构3.1 运行环境和依赖建议使用 Python 3.9 及以上版本。操作系统方面Windows、macOS、Linux 均可但 ffmpeg 的安装方式略有差异。需要安装的 Python 库如下pip install faster-whisper pydub numpyffmpeg 是音频解码和切分的关键工具faster-whisper 解码音频时依赖它。Windows 用户可以从 ffmpeg 官网下载可执行文件然后把 bin 目录加入系统 PATH。macOS 用户可以用 Homebrew 安装brew install ffmpegUbuntu/Debian 用户执行sudo apt update sudo apt install ffmpeg安装完成后可以在终端执行ffmpeg -version验证是否生效。3.2 模型选择说明faster-whisper 支持多种尺寸的模型包括 tiny、base、small、medium、large-v3 等。模型越大识别准确率越高但推理速度越慢内存占用也越高。如果只是处理清晰的中文访谈或课程small或medium是比较合适的选择。如果对准确率要求高且机器配置足够可以尝试large-v3。模型首次运行时需要联网下载下载完成后会缓存在本地后续使用不需要再次下载。3.3 项目文件结构为了让代码结构清晰建议按下面的目录组织项目audio2srt/ ├── main.py # 主入口任务调度 ├── config.py # 配置参数 ├── audio_splitter.py # 音频切分模块 ├── transcribe.py # 转写模块 ├── srt_generator.py # SRT 字幕生成模块 ├── task_manager.py # 任务状态管理续跑核心 ├── input/ # 存放待处理的音视频 ├── output/ # 输出的 SRT 字幕文件 └── progress/ # 任务状态记录本文后续的代码均按该结构编写实际操作时可以根据自己的项目习惯调整。4. 核心模块设计与代码实现4.1 配置文件先创建一个config.py把常用的参数集中管理方便修改。# 文件路径config.py import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) INPUT_DIR os.path.join(BASE_DIR, input) OUTPUT_DIR os.path.join(BASE_DIR, output) PROGRESS_DIR os.path.join(BASE_DIR, progress) # 支持的音视频格式 SUPPORTED_EXTS {.mp3, .wav, .m4a, .flac, .mp4, .mkv, .avi, .mov, .webm} # Whisper 模型参数 MODEL_SIZE small DEVICE cpu COMPUTE_TYPE int8 # 音频切分参数 MIN_SILENCE_DURATION 500 # 最小静音时长毫秒 SILENCE_THRESHOLD -40 # 静音判定阈值dBFS MAX_SEGMENT_DURATION 30 # 单段最大时长秒 KEEP_TRAILING_SILENCE True # 是否保留段尾静音 # 转写参数 LANGUAGE zh BEAM_SIZE 5 VAD_FILTER True # 是否启用语音活动检测过滤静音段 # 输出参数 OUTPUT_FORMAT srt这里需要说明几个关键参数SILENCE_THRESHOLD静音判定阈值单位是 dBFS值越小表示要求越安静才判定为静音。一般-35到-45之间效果不错可以根据实际录音环境调整。MAX_SEGMENT_DURATION切分后单段音频的最大时长避免某一段过长导致模型推理压力过大。VAD_FILTER启用后可以自动过滤掉没有人声的纯静音片段对访谈类音频特别有用。4.2 音频切分模块音频切分模块的作用是把长音频按照静音位置切成多个小片段并返回每个片段在原音频中的起始时间。这里用 pydub 来做静音检测和切分。# 文件路径audio_splitter.py from pydub import AudioSegment from pydub.silence import detect_silence, split_on_silence import config def find_split_points(audio_segment): 找出音频中的静音区间确定切分点。 返回切分点的时间位置列表单位为毫秒。 silence_ranges detect_silence( audio_segment, min_silence_lenconfig.MIN_SILENCE_DURATION, silence_threshconfig.SILENCE_THRESHOLD, ) split_points [] current_position 0 for start, end in silence_ranges: # 如果当前段已经达到最大时长则在此静音处切分 if start - current_position config.MAX_SEGMENT_DURATION * 1000: split_points.append((start end) // 2) current_position (start end) // 2 return split_points def split_audio(input_path): 切分音频文件返回片段列表。 每个片段是一个 dict包含 start_time、end_time 和音频对象。 audio AudioSegment.from_file(input_path) split_points find_split_points(audio) segments [] last_point 0 for point in split_points: segment audio[last_point:point] segments.append({ start_time: last_point, end_time: point, audio: segment, }) last_point point # 最后一段 if last_point len(audio): segments.append({ start_time: last_point, end_time: len(audio), audio: audio[last_point:], }) return segments这个实现的核心逻辑是先检测所有达到最小静音时长的区间然后只选取那些“距离上一个切分点已经超过最大单段时长”的静音位置作为切分点。这样做的好处是既不会把句子切得七零八落也不会让单段音频过长。4.3 转写模块转写模块封装了 faster-whisper 的调用逻辑。为了让批次处理更高效这里采用分段转写的方式将每个切分好的音频片段导出为临时 WAV 文件再交给 faster-whisper 转写并记录该片段相对原文件的偏移量。# 文件路径transcribe.py import tempfile import os from faster_whisper import WhisperModel import config class Transcriber: def __init__(self): self.model WhisperModel( config.MODEL_SIZE, deviceconfig.DEVICE, compute_typeconfig.COMPUTE_TYPE, ) def transcribe_segment(self, audio_segment, start_time_ms): 转写单个音频片段。 audio_segment: pydub AudioSegment 对象 start_time_ms: 该片段在原音频中的起始时间毫秒 返回字幕片段列表时间轴为原音频绝对时间。 # 将 pydub 音频导出为临时 WAV 文件 with tempfile.NamedTemporaryFile(suffix.wav, deleteFalse) as tmp_file: tmp_path tmp_file.name audio_segment.export(tmp_path, formatwav) try: segments, info self.model.transcribe( tmp_path, languageconfig.LANGUAGE, beam_sizeconfig.BEAM_SIZE, vad_filterconfig.VAD_FILTER, ) subtitle_segments [] for seg in segments: subtitle_segments.append({ start: start_time_ms seg.start * 1000, end: start_time_ms seg.end * 1000, text: seg.text.strip(), }) return subtitle_segments finally: os.remove(tmp_path)这里有一个容易被忽略的细节转写返回的seg.start和seg.end是相对当前 WAV 文件的秒数。如果直接把这个时间写入最终字幕时间轴会错乱。所以必须加上start_time_ms偏移量转成原音频的绝对时间。4.4 SRT 文件生成模块SRT 生成模块把时间戳转换成标准格式并写入文件。# 文件路径srt_generator.py def ms_to_srt_time(ms): 将毫秒时间转换为 SRT 时间轴格式HH:MM:SS,mmm hours ms // 3600000 minutes (ms % 3600000) // 60000 seconds (ms % 60000) // 1000 milliseconds ms % 1000 return f{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d} def write_srt(subtitle_segments, output_path): 将字幕片段列表写入 SRT 文件。 subtitle_segments 需要按 start 时间升序排列。 subtitle_segments.sort(keylambda x: x[start]) with open(output_path, w, encodingutf-8) as f: for idx, seg in enumerate(subtitle_segments, start1): start_time ms_to_srt_time(seg[start]) end_time ms_to_srt_time(seg[end]) f.write(f{idx}\n) f.write(f{start_time} -- {end_time}\n) f.write(f{seg[text]}\n\n)4.5 任务状态管理模块续跑功能的实现依赖这个模块。设计思路是程序处理前先生成一个待处理任务列表写入任务文件每完成一个文件就把完成状态写入文件。下次启动时加载任务文件跳过节已完成的任务。# 文件路径task_manager.py import json import os def load_progress(progress_path): 加载任务进度文件。 如果文件不存在返回空字典。 if not os.path.exists(progress_path): return {} with open(progress_path, r, encodingutf-8) as f: return json.load(f) def save_progress(progress_path, progress_data): 保存任务进度到 JSON 文件。 with open(progress_path, w, encodingutf-8) as f: json.dump(progress_data, f, ensure_asciiFalse, indent2) def build_task_list(input_dir, supported_exts): 扫描输入目录生成待处理文件列表。 返回按文件名排序的列表保证处理顺序稳定。 task_list [] for root, dirs, files in os.walk(input_dir): for file in files: ext os.path.splitext(file)[1].lower() if ext in supported_exts: task_list.append(os.path.join(root, file)) return sorted(task_list) def is_task_completed(progress_data, file_path): 判断某个文件是否已经处理完成。 return progress_data.get(file_path) completed4.6 主程序主程序负责把上面的模块串联起来。核心逻辑是加载任务列表。读取进度文件过滤已完成任务。逐个处理未完成任务处理成功后更新进度文件。所有任务完成后输出汇总信息。# 文件路径main.py import os import sys import time import config from audio_splitter import split_audio from transcribe import Transcriber from srt_generator import write_srt, ms_to_srt_time from task_manager import load_progress, save_progress, build_task_list, is_task_completed def process_file(transcriber, input_path, output_path): 处理单个音视频文件生成 SRT 字幕文件。 print(f[处理] {input_path}) # 1. 按静音切分音频 segments split_audio(input_path) print(f[切分] 共切分为 {len(segments)} 段) # 2. 逐段转写 all_subtitles [] total_duration_ms sum(seg[end_time] - seg[start_time] for seg in segments) for idx, seg in enumerate(segments, start1): seg_duration seg[end_time] - seg[start_time] print(f[转写] 第 {idx}/{len(segments)} 段时长 {seg_duration / 1000:.1f}s) start_time time.time() subtitles transcriber.transcribe_segment(seg[audio], seg[start_time]) elapsed time.time() - start_time print(f 已用 {elapsed:.1f}s识别 {len(subtitles)} 条字幕) all_subtitles.extend(subtitles) # 3. 生成 SRT 文件 write_srt(all_subtitles, output_path) print(f[完成] 字幕已保存至 {output_path}) def main(): os.makedirs(config.OUTPUT_DIR, exist_okTrue) os.makedirs(config.PROGRESS_DIR, exist_okTrue) progress_path os.path.join(config.PROGRESS_DIR, task_progress.json) progress_data load_progress(progress_path) task_list build_task_list(config.INPUT_DIR, config.SUPPORTED_EXTS) pending_tasks [t for t in task_list if not is_task_completed(progress_data, t)] if not pending_tasks: print(没有待处理的任务全部已完成。) return print(f待处理任务数{len(pending_tasks)}) transcriber Transcriber() for task_path in pending_tasks: try: rel_path os.path.relpath(task_path, config.INPUT_DIR) base_name os.path.splitext(rel_path)[0] output_path os.path.join(config.OUTPUT_DIR, base_name .srt) os.makedirs(os.path.dirname(output_path), exist_okTrue) process_file(transcriber, task_path, output_path) # 更新任务状态 progress_data[task_path] completed save_progress(progress_path, progress_data) except Exception as e: print(f[错误] 处理 {task_path} 时发生异常{e}) print(跳过该文件继续处理下一个任务。) print(所有任务处理完成。) if __name__ __main__: main()这个主程序已经具备完整的批量处理和续跑能力。程序每处理完一个文件就会立刻把状态写入 JSON 文件。即使中途崩溃已完成文件的进度也不会丢失。5. 完整运行演示5.1 准备测试文件在input目录下放入两到三个音视频文件比如input/ ├── 课程01.mp4 ├── 访谈02.m4a └── 会议记录03.mp35.2 运行程序在项目根目录执行python main.py首次运行会下载模型网络较慢时可能需要等待一段时间。模型下载完成后程序会自动开始处理。5.3 预期输出正常情况下控制台输出类似这样待处理任务数3 [处理] /path/to/input/课程01.mp4 [切分] 共切分为 12 段 [转写] 第 1/12 段时长 28.3s 已用 15.2s识别 6 条字幕 [转写] 第 2/12 段时长 30.0s 已用 17.8s识别 5 条字幕 ... [完成] 字幕已保存至 /path/to/output/课程01.srt全部完成后output目录下会生成对应的 SRT 文件output/ ├── 课程01.srt ├── 访谈02.srt └── 会议记录03.srt5.4 续跑验证可以手动中断程序来验证续跑功能。只要程序在处理完完整文件后才更新进度那么中断后再次运行已经完成的任务会自动跳过只会处理剩余文件。比如上面的示例中如果课程01.srt已经生成而访谈02.m4a处理到一半被中断重新运行程序后控制台会显示待处理任务数2 [处理] /path/to/input/访谈02.m4a ...课程01.mp4不会再被重复处理这就是续跑带来的效率提升。6. 常见问题与排查思路6.1 常见错误对照表问题现象常见原因解决思路导入 pydub 时提示找不到 ffmpegffmpeg 未安装或未加入系统 PATH安装 ffmpeg执行ffmpeg -version检查转写速度很慢模型过大或 CPU 性能不足改用 small/base 模型或开启 int8 量化字幕时间轴错乱转写时间未加偏移量检查transcribe_segment中是否加上start_time_ms识别内容为空音频音量太低或语言参数不对增大音量检查LANGUAGE参数打开 VAD 过滤检测静音失败没有切分点静音阈值设置不合理调高SILENCE_THRESHOLD到 -30 或 -25程序报错提示磁盘空间不足临时 WAV 文件堆积检查临时文件目录确认代码中已删除临时文件6.2 音频切分效果不佳怎么办切分效果与录音环境密切相关。如果录音中存在明显的环境噪声静音检测可能失效导致切分点过少或过密。遇到这种情况可以先做一个简单的降噪预处理from pydub import AudioSegment from pydub.effects import normalize audio AudioSegment.from_file(input_path) audio normalize(audio)normalize会把音频的峰值音量归一化能让后续的静音检测更稳定。6.3 内存占用过高怎么办如果一次处理时长很长的音频pydub 会把整个音频加载到内存中导致内存占用飙升。对于超过 1 小时的音频建议先用 ffmpeg 按固定时间窗口切割成若干中间文件再逐个调用AudioSegment.from_file加载。这个策略虽然多做了一步文件操作但内存消耗会稳定很多。7. 最佳实践与工程建议7.1 合理设计任务状态更新时机续跑不是简单地加一个 try-except 就能实现。真正的关键点是任务状态必须在“整个文件的字幕文件成功写入磁盘之后”更新而不是在转写开始时更新。这样即使转写中途崩溃下次启动也能重新处理不会生成残缺的字幕文件。7.2 输出文件命名规范建议输出 SRT 文件时保持与源文件相同的相对路径这样既避免重名覆盖又方便管理。对于不同语言的翻译需求可以在文件名后追加语言后缀例如课程01.zh.srt、课程01.en.srt。7.3 日志与可观测性批量处理任务往往耗时较长如果缺少日志很难定位问题。建议在关键节点加入日志输出并写入独立的日志文件。Python 的logging模块可以很方便地实现控制台和文件双输出。7.4 隐私与安全在本地运行 AI 模型的优势之一是不需要把数据上传到第三方服务。如果你的素材涉及隐私信息或商业机密本地推理是更安全的选择。需要注意模型文件本身是公开的输入内容不会离开本机这一点在向团队推广时可以重点说明。7.5 模型选择与成本控制如果只是轻量使用base和small模型已经能覆盖大部分中文场景。medium和large-v3虽然在复杂口音、背景噪声场景下表现更好但推理时间会成倍增加。建议先用少量样本测试不同模型的效果和耗时再正式批量运行。8. 扩展方向当前版本的工具已经具备完整的“批量处理、长音频切分、续跑”三大能力。如果想进一步实用化可以考虑下面几个方向多格式输出在 SRT 之外同时输出 VTT、纯文本 TXT 或带说话人标签的 JSON。说话人分离接入 pyannote 等说话人分离模型让字幕区分“人物 A / 人物 B”。并行加速在 CPU 多核环境下可以同时对多个文件启动转写进程进一步缩短总耗时。定时任务把脚本封装成可执行文件配合系统自带定时任务在夜间自动处理素材。字幕翻译对转写文本调用本地翻译模型实现字幕自动翻译方便做双语字幕。本文给出的代码完全可以在本地直接运行。核心价值在于把“音频切分、模型转写、字幕生成、状态管理”这四个环节拆成独立模块每一块都可以按你的实际场景替换或增强。如果你在做批量字幕处理时还有其他问题欢迎在评论区交流我会根据实际问题继续补充更深入的实践方案。