by 雪隐_上班了 from juejin.cn/user/143341...
欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权。
whisper_v3 核心代码解析:如何从视频自动生成字幕
前言
上一篇文章我们聊了 Whisper 是什么、能干什么、以及各模型怎么选。
这一篇不废话,直接开膛破肚------看看 whisper_v3 项目的核心代码,怎么把一个没字幕的烂视频,变成带时间戳的 SRT 字幕文件。
整个项目就两个模块,分工明确:
- VideoSubtitleGenerator --- 负责"听写":视频 → 音频 → 语音识别 → SRT 字幕
- SubtitleCorrector --- 负责"翻译":把英文字幕变成中文(可选,非必须)
友情提示:看不懂代码没关系,能看懂段子就行。反正你最后也是复制粘贴跑。
一、环境准备:先把工具磨好
跑代码之前,有几个东西必须先装上,否则后面会报错报到你想砸电脑。
1.1 FFmpeg:音视频界的瑞士军刀
字幕生成的第一步是提取音频,这活儿全靠 FFmpeg。
Windows 安装(三选一,哪个顺手用哪个):
bash
# 方法一:winget(最省心)
winget install Gyan.FFmpeg
# 方法二:chocolatey(如果你装了)
choco install ffmpeg
# 方法三:手动下载,扔进 PATH
# 下载地址自己搜,解压后把 bin 目录加到系统环境变量
装完验证一下:
bash
ffmpeg -version
# 如果输出版本号而不是"不是内部命令",恭喜你,成功迈出第一步。
有些人觉得 FFmpeg 难装。其实不难,就是环境变量里添个路径。比追女生简单多了------至少 FFmpeg 不会猜你为什么没放对位置。
1.2 下载 Whisper 模型(记得选对格式)
本项目用的是 Faster-Whisper(CTranslate2 格式),不是原生 PyTorch 模型。别下错了。
bash
# 推荐 large-v3,效果最好,你的 5060 Ti 16G 刚好装下
https://huggingface.co/Systran/faster-whisper-large-v3
# 如果你显卡只有 8G,选 medium 或 small
https://huggingface.co/Systran/faster-whisper-medium
https://huggingface.co/Systran/faster-whisper-small
下载后解压,得到一个文件夹,里面是 model.bin 之类的文件。
把文件夹路径记下来,待会儿填进 config.py 的 MODEL_PATH。
下载模型的时候可以去泡杯咖啡。如果泡完咖啡还没下完,说明你网速不如 56K 猫,建议换时间再下。
1.3 安装 Python 依赖
bash
pip install faster-whisper pysrt openai
faster-whisper:语音识别引擎,比原版快 2-4 倍(显卡表示很欣慰)pysrt:读写 SRT 字幕文件(不用自己正则硬拆,少掉头发)openai:对接 DeepSeek API 或本地 LLM(翻译用)
1.4 翻译模型配置(二选一,非必须)
如果你只需要英文字幕,这步可以跳过。但大多数人都想转中文,那就接着看。
方式一:DeepSeek API(推荐,省心)
- 去 DeepSeek 官网 注册,获取 API Key(新用户送额度,真香)
- 填到
config.py的DEEPSEEK_API_KEY - 设置
LOCAL_LLM_ENABLED = False
方式二:本地 LLM(完全离线,装X专用)
- 下载 LM Studio
- 找一个支持中文翻译的模型,比如
Qwen2-7B-Instruct、Llama3-8B或者翻译特化的hy-mt2-7b - 在 LM Studio 里加载模型,开启本地 API 服务(默认
http://127.0.0.1:1234/v1) - 修改
config.py:LOCAL_LLM_ENABLED = True,填入模型名称
本地 LLM 的优点是隐私,缺点是慢。你花 10 分钟生成字幕,再花 1 小时翻译------正好可以去吃个午饭。
1.5 最后的配置文件:core/config.py
修改几个关键参数:
python
# 模型路径(解压后的文件夹,不是 .bin 文件本身)
MODEL_PATH = r"D:\work\models\faster-whisper-large-v3"
# 运行设备:有 NVIDIA 显卡就写 "cuda",否则 "cpu"(等得你心碎)
DEVICE = "cuda"
# DeepSeek API 方式
DEEPSEEK_API_KEY = "sk-xxxxxxxxxxxx"
LOCAL_LLM_ENABLED = False
# 或者本地 LLM 方式(上面那个要改成 True)
# LOCAL_LLM_ENABLED = True
# LOCAL_LLM_API_BASE = "http://127.0.0.1:1234/v1"
# LOCAL_LLM_MODEL_NAME = "qwen2-7b-instruct"
配置好了就别再动,除非你换了显卡。
二、视频字幕生成引擎:core/video_to_subtitle.py
这个模块负责把视频变成 SRT 字幕。核心就一个类:VideoSubtitleGenerator,里面几个方法各司其职。
2.1 整体流程(四步走)
python
def generate_subtitle(self, video_path, output_srt=None, language=None, keep_audio=False):
# 1. 音频提取
audio_path = self.extract_audio(video_path)
# 2. Whisper 语音识别,返回带时间戳的片段
result = self.transcribe_with_timestamp(audio_path, language)
# 3. 合并重复字幕(Whisper 有时会重复说同一句话)
result["segments"] = self.merge_duplicate_subtitles(result["segments"])
# 4. 生成 SRT 文件
self.create_srt(result["segments"], output_srt)
你可以把这四步想象成:剥壳 → 听写 → 纠错 → 抄正。小学生改作文都没这么认真。
2.2 音频提取:extract_audio() ------ 先检查视频有没有声音
很多新手踩的第一个坑:视频本身没音轨,Whisper 对着空气识别半天,输出空字幕。
python
def extract_audio(self, video_path: str, output_audio: str = None) -> str:
# 先用 ffprobe 检查视频有没有音频轨道
cmd_check = [
"ffprobe", "-v", "error", "-select_streams", "a",
"-show_entries", "stream=codec_type",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
result = subprocess.run(cmd_check, capture_output=True, text=True, check=True)
audio_streams = result.stdout.strip()
if not audio_streams:
raise ValueError(f"文件 '{os.path.basename(video_path)}' 中没有音频流...")
如果这一步报错,别怪代码。是你的视频不够"声情并茂"。
检查通过后,用 ffmpeg 提取音频:
python
cmd = [
"ffmpeg", "-i", video_path,
"-vn", # 不要视频流
"-acodec", "pcm_s16le", # PCM 16bit
"-ar", "16000", # 16kHz 采样率
"-ac", "1", # 单声道
"-y", output_audio
]
16kHz 单声道是 Whisper 训练时用的格式,必须匹配。你非要用 44.1kHz 立体声,Whisper 也能跑,但准确率会下降,就像让一个学英语的人去考法语四级。
2.3 语音识别:transcribe_with_timestamp() ------ 真正的核心
这里用的是 Faster-Whisper,不是 OpenAI 原版。为什么?因为快。
python
def _load_model(self):
from faster_whisper import WhisperModel
self.pipe = WhisperModel(
self.model_path,
device=self.device,
compute_type="float16" if self.device == "cuda" else "float32"
)
float16 半精度计算,显存占用减半,速度翻倍------5060 Ti 的 16G 显存在这件事上显得格外帅气。
然后开始识别:
python
def transcribe_with_timestamp(self, audio_path: str, language=None):
self._load_model()
segments, info = self.pipe.transcribe(
audio_path,
language=language or config.WHISPER_LANGUAGE or "en",
word_timestamps=True,
vad_filter=True, # 语音活动检测,过滤静音和背景音乐
vad_parameters=dict(
min_silence_duration_ms=500, # 静音超过 500ms 算断句
speech_pad_ms=200, # 每段前后加 200ms 缓冲
min_speech_duration_ms=100
)
)
vad_filter=True:Voice Activity Detection。没有它,Whisper 连背景里的猫叫都会识别成"喵喵喵"。speech_pad_ms=200:防止语音被截断,相当于给每句话加了个安全气囊。
返回的 segments 是一个迭代器,每个元素是 {start, end, text}。
2.4 字幕去重合并:merge_duplicate_subtitles() ------ 专治 Whisper 的碎碎念
Whisper 有时候会把同一句话切分成多个相似的片段,比如:
csharp
[0.0-1.0] 今天我们来聊聊
[1.0-2.0] 我们来聊聊 Python
[2.0-3.0] Python 装饰器
这显然是病,得治。合并算法基于 最长公共子序列(LCS):
python
def merge_duplicate_subtitles(self, segments, similarity_threshold=0.85):
merged = []
i = 0
while i < len(segments):
current = segments[i]
merged_text = current["text"]
merged_start = current["start"]
merged_end = current["end"]
# 往后看,能合并就合并
while i + 1 < len(segments):
next_seg = segments[i + 1]
is_identical = merged_text == next_seg["text"]
similarity = self._calculate_similarity(merged_text, next_seg["text"]) if not is_identical else 1.0
if is_identical or similarity >= similarity_threshold:
merged_end = max(merged_end, next_seg["end"])
i += 1
continue
break
merged.append({"start": merged_start, "end": merged_end, "text": merged_text})
i += 1
return merged
相似度计算靠 LCS 长度除以较长字符串长度。简单说:如果两句话 85% 的字符都一样,就认为是在重复。
你可能会问:为什么要手写 LCS,不用
difflib?答:因为手写显得我比较硬核。实际上就是懒得加依赖。
2.5 SRT 文件生成:create_srt() ------ 最后一步,也是最稳的一步
python
def create_srt(self, segments, output_srt):
subs = pysrt.SubRipFile()
for i, segment in enumerate(segments, 1):
start, end = segment["start"], segment["end"]
# 修复无效时间戳:end <= start 时强行给 0.5 秒
if end <= start:
end = start + 0.5
# 跳过异常片段:单条超过 30 秒(通常是识别错了)
if end - start > 30:
continue
sub = pysrt.SubRipItem()
sub.index = i
sub.start = timedelta(seconds=start)
sub.end = timedelta(seconds=end)
sub.text = segment["text"]
subs.append(sub)
subs.save(output_srt, encoding='utf-8')
两个防御:
end <= start→ 补 0.5 秒,避免播放器崩溃end - start > 30→ 直接丢弃,这种超长片段 99% 是识别错误(比如把整段背景音乐当成语音)
有些人问:为什么不调大阈值到 60 秒?
答:30 秒一句话,你是播音员还是唐僧?
三、字幕翻译校正器:core/subtitle_corrector.py
如果你只需要英文字幕,这段可以不看。但如果你像我一样,看英文就想睡觉,那翻译模块就是你的救星。
3.1 双后端初始化:支持 DeepSeek 和本地 LLM
python
def __init__(self, api_key=None, api_base=None, use_local_llm=None):
if use_local_llm is None:
self.use_local_llm = config.LOCAL_LLM_ENABLED
else:
self.use_local_llm = use_local_llm
if self.use_local_llm:
self.client = OpenAI(api_key="dummy", base_url=config.LOCAL_LLM_API_BASE)
self.model_name = config.LOCAL_LLM_MODEL_NAME
else:
self.client = OpenAI(api_key=api_key, base_url=api_base)
本地 LLM 需要填一个假的 api_key,因为 LM Studio 不验证 key。
这个设计很像某些小区的门禁------刷什么都行,只要刷了就行。
3.2 批量翻译:translate_srt_file() ------ 既快又稳
python
def translate_srt_file(self, input_srt, output_srt, progress_callback=None, batch_size=200):
subs = pysrt.open(input_srt, encoding='utf-8')
total_count = len(subs)
# 本地 LLM 每次只能处理 5 条,云端 API 可以 200 条
effective_batch_size = config.LOCAL_LLM_BATCH_SIZE if self.use_local_llm else batch_size
translated_subs = pysrt.SubRipFile()
for i in range(0, total_count, effective_batch_size):
batch = subs[i:i + effective_batch_size]
try:
translated_batch = self._translate_batch(batch)
translated_subs.extend(translated_batch)
except Exception as e:
# 翻译失败保留原文,不中断流程
print(f"批次翻译失败,保留原文: {e}")
translated_subs.extend(batch)
translated_subs.save(output_srt, encoding='utf-8')
核心思想:分批翻译 + 失败保底。
不指望 LLM 每次都完美,但至少别让整个流程崩掉。就像考试不会的题先空着,别把整张卷子撕了。
3.3 批量翻译核心:_translate_batch() ------ 和 LLM 斗智斗勇
python
def _translate_batch(self, batch):
texts_to_translate = [sub.text for sub in batch]
combined_text = "\n".join([f"[{i+1}] {text}" for i, text in enumerate(texts_to_translate)])
prompt = f"""请将以下{len(batch)}条字幕翻译成中文。
要求:
1. 严格按顺序翻译,每条字幕对应一行翻译结果
2. 必须输出恰好{len(batch)}行翻译,一行不多一行不少
3. 只返回翻译结果,不要任何解释、序号、前缀
4. 保持口语化风格,专业术语准确
待翻译:
{combined_text}
请输出恰好{len(batch)}行翻译:"""
这里的技巧是用 [序号] 原文 告诉 LLM 每条的顺序,并且强制要求输出行数等于输入行数。
这就好比你跟服务员说:"我要三个菜,你给我端三个盘子来,不许合并,不许少,不许多。"------LLM 就是那个经常试图合并菜品的服务员。
python
response = self.client.chat.completions.create(
model=self.model_name,
messages=[
{"role": "system", "content": "你是一个专业的字幕翻译..."},
{"role": "user", "content": prompt}
],
temperature=0.3,
max_tokens=8000,
timeout=120 if self.use_local_llm else 30
)
# 验证行数
translated_lines = [line.strip() for line in response.choices[0].message.content.split('\n') if line.strip()]
if len(translated_lines) != len(texts_to_translate):
# 重试,用更低温度
retry_prompt = f"请严格输出{len(texts_to_translate)}行翻译,每行对应一条字幕,不要序号。\n{combined_text}"
retry_response = self.client.chat.completions.create(
model=self.model_name,
messages=[{"role": "system", "content": "你是专业字幕翻译,必须输出指定行数。"}, {"role": "user", "content": retry_prompt}],
temperature=0.1,
max_tokens=8000
)
translated_lines = [line.strip() for line in retry_response.choices[0].message.content.split('\n') if line.strip()]
行数验证是整段代码的灵魂。LLM 经常自作聪明合并相似句子,或者漏掉某一行。发现行数不对,立刻用更低温度重试------温度 0.1 几乎等于让 LLM 当复读机。
最后用正则把序号前缀去掉:
python
translated = re.sub(r'^\d+[\.\、]\s*', '', translated)
new_sub.text = translated
这样 1. 今天天气不错 就变成 今天天气不错。干净利落。
四、技术亮点(吹牛时间)
4.1 VAD 语音活动检测:不再把猫叫当对话
没开 VAD 的时候,Whisper 连背景里的咳嗽声都会识别成"嗯哼"。
开启 vad_filter=True 后,只有人声片段才会被识别。
效果就像给你的视频装了个"只录取人声"的过滤器,猫主子再也不能抢戏。
4.2 时间戳安全修复:避免 SRT 播放崩溃
SRT 格式要求每条字幕的结束时间必须大于开始时间。Whisper 偶尔会生成 end <= start 的无效片段,播放器遇到这种直接罢工。
我们的代码会强制修复成 0.5 秒最小长度,并且丢弃超过 30 秒的异常片段。
这叫 "数据清洗,从我做起"。
4.3 翻译失败保底:宁可保留原文,不丢字幕
调用 LLM 翻译时,网络抖动、API 限流、模型抽风......都可能失败。
代码捕获异常后,直接把英文原文保留在原位置。
你得到的结果可能中英混杂,但至少每句话都在。比某度翻译那种"丢失半句"要强一百倍。
五、总结
whisper_v3 的核心代码其实就做两件事:
第一件事:视频 → 字幕
- FFmpeg 检查音轨 → 提取 16kHz 单声道音频
- Faster-Whisper + VAD 识别 → 带时间戳的片段
- LCS 去重合并 → pysrt 生成标准 SRT
第二件事:字幕翻译
- 批量请求 LLM(DeepSeek API 或本地 LM Studio)
- 行数验证 + 重试机制(防止 LLM 偷工减料)
- 失败时保留原文,绝不丢字幕
全程跑在你的 5060 Ti 16G 上,数据不出门,隐私有保障。
对比那些需要上传视频到云端的付费服务,你不仅省了钱,还避免了"你的教学视频出现在别人的训练集里"这种社死事件。
致谢 & 参考:
- OpenAI Whisper --- 开源语音识别的扛把子
- Faster-Whisper --- 让 Whisper 跑得像火箭
- PySRT --- 让你不用手撕 SRT 格式
如果这篇文章让你在写代码时笑出了声,请点赞、评论、转发 。
你们的笑声是我继续熬夜的唯一动力(咖啡不算,咖啡因已经免疫了)。
谢谢大家 🙏
祝你们的字幕永远对齐,翻译从不抽风。