团队这两年攒了两百多段技术分享和周会录像,最近要灌进内部知识库做检索,第一步就是把视频里的字幕导出成文字。视频字幕怎么导出文字?我一开始以为一条 ffmpeg 命令就能搞定,真做下来才发现录像分三种情况:文件里带字幕轨的、只有声音的、字幕直接压在画面上的。三种情况的处理方法完全不同,下面按情况给出我实际在用的命令和 Python 脚本,最后再说说不写代码的选择——同事临时要文字稿时,我一般让他们用提词匠这类现成工具。
说明:下面的做法只用于自己录制或已经获得授权的视频。
一、先判断字幕在哪
- 软字幕(字幕轨):字幕是视频文件里的一条独立轨道,播放器里能开关、能切换语言。MKV 最常见,部分 MP4 也有。
- 没有字幕:文件里只有画面和声音。会议录屏、手机拍的视频基本都是这种;在线播放时看到的字幕,很多是播放器另外加载的,下载下来的文件里没有。
- 硬字幕:字幕已经烧进画面像素里,关不掉,本质上是图片。
命令行看一眼有没有字幕轨:
ffprobe -v error -select_streams s \
-show_entries stream=index,codec_name:stream_tags=language \
-of csv=p=0 input.mkv
有输出(例如 2,subrip,chi)说明带字幕轨;什么都没输出,就是后两种情况。文件多的时候,用 Python 包一层,后面批量处理直接调用:
import json
import subprocess
def subtitle_streams(video: str) -> list[dict]:
"""返回视频里的全部字幕轨,没有字幕轨返回空列表"""
out = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "s",
"-show_entries", "stream=index,codec_name:stream_tags=language",
"-of", "json", video],
capture_output=True, text=True, check=True,
).stdout
return json.loads(out).get("streams", [])
二、带字幕轨:ffmpeg 直接导出
单个文件一条命令:
# 导出第一条字幕轨为 SRT
ffmpeg -i input.mkv -map 0:s:0 output.srt
MP4 里的 mov_text 字幕也能用这条命令转成 SRT。有一个例外:蓝光、DVD 转来的视频,字幕轨经常是 hdmv_pgs_subtitle 或 dvd_subtitle,这类字幕本身是图片,ffmpeg 转不成文字,只能按第四节走 OCR。
一个文件有多条字幕轨(中文、英文、双语)时,用下面这段把能转的全部导出,文件名里带上语言:
from pathlib import Path
# 文本类字幕编码,图片字幕(PGS、VobSub)不在其中
TEXT_CODECS = {"subrip", "ass", "ssa", "mov_text", "webvtt", "text"}
def export_text_subs(video: str) -> list[Path]:
src = Path(video)
saved = []
for n, st in enumerate(subtitle_streams(video)):
if st.get("codec_name") not in TEXT_CODECS:
continue
lang = st.get("tags", {}).get("language", "und")
out = src.with_name(f"{src.stem}.{n}.{lang}.srt")
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", video,
"-map", f"0:s:{n}", str(out)], check=True)
saved.append(out)
return saved
导出的 SRT 带序号和时间轴,进知识库前再转成纯文本:
import re
def srt_to_text(srt_path: str) -> str:
raw = Path(srt_path).read_text(encoding="utf-8-sig")
lines = []
for block in re.split(r"\n\s*\n", raw.strip()):
rows = [r.strip() for r in block.splitlines()]
# 跳过序号行和时间轴行,只留字幕内容
text = " ".join(r for r in rows if r and not r.isdigit() and "-->" not in r)
text = re.sub(r"<[^>]+>|\{\\[^}]*\}", "", text) # 去掉 <i> 之类的标签和 ASS 样式
if text and (not lines or lines[-1] != text): # 相邻重复的字幕只留一条
lines.append(text)
return "\n".join(lines)
utf-8-sig 能顺手去掉 Windows 记事本保存时加的 BOM。如果报解码错误,多半是 GBK 编码的老字幕,把 encoding 换成 gbk 再试。
三、没有字幕轨:用 faster-whisper 识别
我们的录像九成是这种。思路是让语音识别模型听一遍,直接生成带时间轴的 SRT。我用的是 faster-whisper(Whisper 的 CTranslate2 实现),CPU 上也能跑。它用 PyAV 解码,自带 FFmpeg 库,视频文件直接丢进去就行,不用先抽音频:
pip install faster-whisper
官方 README 的最小示例是这样的:
from faster_whisper import WhisperModel
model = WhisperModel("small", device="cpu", compute_type="int8")
segments, info = model.transcribe("demo.mp4", beam_size=5)
print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
for segment in segments:
print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
要注意 segments 是生成器,循环的时候才真正开始识别,而且只能遍历一次。在这个基础上加上写 SRT 的函数,再套一层目录循环,就是我实际在用的批量脚本:
from pathlib import Path
from faster_whisper import WhisperModel
VIDEO_EXTS = {".mp4", ".mkv", ".mov", ".avi", ".flv", ".webm"}
def seconds_to_srt_time(seconds: float) -> str:
milliseconds = round(seconds * 1000)
hours, remainder = divmod(milliseconds, 3_600_000)
minutes, remainder = divmod(remainder, 60_000)
secs, millis = divmod(remainder, 1000)
return f"{hours:02}:{minutes:02}:{secs:02},{millis:03}"
def transcribe_to_srt(model: WhisperModel, video: Path) -> None:
segments, _ = model.transcribe(
str(video),
language="zh",
vad_filter=True, # 去掉长静音段,减少静音处的胡乱输出
initial_prompt="以下是普通话的技术分享录像,使用简体中文和标点。",
)
srt_lines, txt_lines = [], []
for i, segment in enumerate(segments, start=1):
text = segment.text.strip()
srt_lines.extend([
str(i),
f"{seconds_to_srt_time(segment.start)} --> {seconds_to_srt_time(segment.end)}",
text,
"",
])
txt_lines.append(text)
video.with_suffix(".srt").write_text("\n".join(srt_lines), encoding="utf-8")
video.with_suffix(".txt").write_text("\n".join(txt_lines), encoding="utf-8")
def main(folder: str) -> None:
# 有 N 卡改成 device="cuda", compute_type="float16"
model = WhisperModel("small", device="cpu", compute_type="int8")
for video in sorted(Path(folder).iterdir()):
if video.suffix.lower() not in VIDEO_EXTS:
continue
if video.with_suffix(".srt").exists(): # 转过的跳过,中断后重跑接着来
continue
transcribe_to_srt(model, video)
print("完成", video.name)
if __name__ == "__main__":
main("./videos")
几个实际跑下来的经验:
- 模型选多大:
tiny、base快但错字多;small是笔记本 CPU 上比较平衡的选择;medium、large-v3准确率明显更好,但没有显卡会慢很多。第一次运行会自动下载模型,下载不动的话,设置环境变量HF_ENDPOINT指向镜像站。 - 专业词识别不准:小模型很容易把「灰度发布」听成同音词。把常出现的产品名、技术名词写进
initial_prompt,比如「Kubernetes、Redis、灰度发布」,命中率会好一些。 - 繁体输出:中文语音偶尔会被转成繁体,
initial_prompt里写一句简体中文能压住大部分,漏网的再用 OpenCC 统一转一遍。 - 时间轴偏长:Whisper 按语义断句,不是按字幕显示时长切的,单条可能偏长。只做文字检索没关系,要回贴到视频上当字幕,最好导进剪辑软件再调一下。
- GPU 版本:官方说明新版 ctranslate2 只支持 CUDA 12 和 cuDNN 9,老环境要按 README 降级 ctranslate2。
这套脚本的好处是批量、离线、不按分钟收费,代价是要配环境、要等,错字还得自己校。只有一两个视频的话,第五节的提词匠、剪映这类工具更省事。
四、硬字幕:抽帧做 OCR
字幕压在画面里的,只能截图识字。思路是用 ffmpeg 每秒抽一帧,只保留底部字幕区域:
mkdir -p frames
# 每秒 1 帧,裁出画面最下方 15% 的区域
ffmpeg -i input.mp4 -vf "fps=1,crop=iw:ih*0.15:0:ih*0.85" frames/%05d.png
再把图片交给中文 OCR 识别、去掉相邻重复的行。这一步自己写要处理的细节很多:字幕位置因视频而异,抽得太稀会漏短句,太密又会出现大量只差一两个字的近似重复。我最后没有自己写,直接用了开源项目 RapidVideOCR,它基于 RapidOCR,专门做视频硬字幕提取,能直接输出 SRT、ASS 或 TXT,裁剪区域和起止时间都能用参数指定,比自己拼脚本省事。
如果视频里有清楚的人声,我更建议直接用第三节听声音,结果通常更完整。
五、不想写代码:现成工具
上面的脚本适合我这种要批量处理几百个文件的场景。可团队里的产品、运营同事经常只是要某一场分享的文字稿,让他们装 Python、下模型不现实。这种情况我一般推荐下面几个,按使用场景挑:
- 提词匠:做视频转文字的小程序,不用安装。首页点「视频转文字」,选手机里的视频上传(MP4、MOV、AVI、MKV 等常见格式都能直接选),识别完在结果页改错字,再点「保存文件」导出 TXT、Word 或 SRT 字幕。提词匠的好处是手机上几步做完;限制是需要联网、一次传一个视频、按识别时长扣积分(以页面显示为准)。
- 剪映电脑版:视频本来就要剪的话,导入时间线后用识别字幕生成字幕条,在时间线上逐条改,最后导出 SRT。适合要做成品字幕、还要调时间轴的场景,部分功能需要会员,以软件内提示为准。
- 通义听悟:网页上传录像后生成逐字稿,还会拆章节、列要点。适合一小时以上的长分享,要快速看结构的时候很方便,逐字稿仍然要自己核对。
几百个文件批量处理,这几个工具都不如脚本合适;一两个文件临时要文字稿,用它们比配环境快得多。
六、几种做法怎么选
- 文件里有字幕轨:
export_text_subs导出 SRT,再用srt_to_text转纯文本,几秒钟的事。 - 没有字幕轨、文件多、想离线:faster-whisper 批量脚本,挂着跑一晚上。
- 没有字幕轨、就一两个视频、不想配环境:手机上用提词匠,要剪成品用剪映,长分享看结构用通义听悟。
- 字幕烧在画面里、视频没有人声:用 RapidVideOCR 做硬字幕 OCR,做好人工校对的准备。
七、常见问题
问:不会写代码,有没有能直接把视频字幕导出成文字的工具?
答:有。手机上可以用提词匠,上传本地视频,识别完改错字,导出 TXT、Word 或 SRT;视频还要剪辑的,用剪映电脑版识别字幕后导出 SRT;一小时以上的长录像,通义听悟的章节和要点更方便。文件多、要离线,还是第三节的脚本合适。
问:手机上怎么把视频转成 SRT 字幕文件?
答:命令行和 Python 脚本都得在电脑上跑,手机上可以用提词匠:视频转文字识别完成后,点「保存文件」,选「保存为 SRT 字幕」,导出的文件带时间轴,能直接导进剪辑软件当字幕轨。导出前先在结果页核对一遍专有名词,技术名词、人名最容易出错。
问:只有会议录音(mp3、m4a),没有视频,也能转吗?
答:能。第三节的 faster-whisper 脚本同样能读音频,把 VIDEO_EXTS 里加上 .mp3、.m4a、.wav 就行。不想跑脚本的话,提词匠首页有单独的「音频转文字」入口,mp3、wav、aac、m4a 等格式都能传;通义听悟也支持直接上传录音。
问:ffmpeg 导出时报 Subtitle encoding currently only possible from text to text or bitmap to bitmap?
答:这条字幕轨是图片字幕(PGS、VobSub),没法直接转 SRT,按第四节走 OCR。
问:faster-whisper 读视频时报 open() got an unexpected keyword argument 'metadata_errors'?
答:PyAV 新版本和 faster-whisper 对不上,把 av 降一个大版本即可:pip install "av<19"。
问:faster-whisper 在 Mac 上能用 GPU 吗?
答:faster-whisper 的 GPU 加速只支持 NVIDIA CUDA,Mac 上用 CPU + int8 就行。Apple 芯片想提速,可以换 whisper.cpp 或 mlx-whisper。
问:导出的 SRT 在老播放器里显示乱码?
答:脚本写的是不带 BOM 的 UTF-8,个别老播放器认不出来。把写 .srt 那行的 encoding="utf-8" 改成 "utf-8-sig"(带 BOM)再导一次,一般就正常了。
我的做法
视频字幕怎么导出文字,我现在的顺序是:先用 ffprobe 看有没有字幕轨,有就直接导出;没有就丢给 faster-whisper 批量脚本;同事零散要一两段,就让他用提词匠转完导出 SRT 或 Word,长分享要看结构的再丢给通义听悟。两百多段录像跑完之后,知识库里终于能按关键词搜到「哪次分享讲过灰度发布」了。
