用Flow打造视频处理自动化流水线:切片、字幕、封面一键生成

📅 发布时间:2026/9/1 10:19:39
用Flow打造视频处理自动化流水线:切片、字幕、封面一键生成 做短视频、口播视频和直播回放时最耗时间的往往不是拍摄而是反复处理同一批素材。“Google Flow”在创作者圈里被提到时很多人以为它只是一个简单的视频合并工具其实里面藏了不少影片小工具从自动切片、字幕生成到封面导出都能用流程化方式组合起来。这篇文章会从工程角度把这些小工具拆开给你一套可以复现的视频处理 Flow 工作流。如果你平时要做批量视频处理、课程切片、短剧剪辑或者 AI 辅助创作下面的内容可以直接模仿。先用一句话交代边界不同版本和入口里Flow 的界面和叫法不一定相同所以这里以“工作流节点 命令行工具”的方式实现你拿到自己的环境中也能落地。这类工作流的本质是把重复劳动交给脚本让人只处理真正需要判断的部分。下面先解释 Google Flow 里的影片小工具到底在解决什么问题再按“拆流程 - 搭环境 - 写工具 - 跑验证 - 排错 - 上生产”的顺序逐步搭出一条最小可运行的视频创作流水线。1. 先理解 Google Flow 的影片小工具在解决什么问题1.1 Flow 是什么影片小工具又是什么Flow 的核心含义是“把固定步骤编排成一条可重复执行的工作流”。视频创作看起来是创意工作但其中大量步骤是重复的导入素材、裁掉片头片尾、去掉静音、加字幕、生成封面、转成不同比例。Flow 把这些步骤抽象成一个个独立节点每个节点只做一件事节点与节点之间通过文件和清单传递结果组合起来就是一条流水线。影片小工具可以理解为这条流水线上的功能节点。常见的节点包括切片节点输入一个视频文件和一组时间区间输出多个分段视频。字幕节点输入一条音轨通过语音识别生成字幕文件。封面节点输入一行标题和一张底图输出不同平台尺寸的封面。转码节点输入一个 MP4输出适合 YouTube、视频号、短视频平台的格式。这些节点不是必须绑定在一个大软件里它们可以是命令、脚本也可以是 Flow 编辑器里的可视化卡片。理解了这一点后面写代码时就不会被界面限制住。1.2 为什么它能省下一半创作时间“省一半时间”的关键不是某一个工具速度快而是“不用重复做同样动作”。举例一期 10 分钟的口播视频常规操作是导进剪辑软件、放大时间线、手动切掉空白段、等语音识别字幕、再导出横版和竖版封面。一次做三五个视频还能接受如果每周要出二十条重复操作就非常消耗精力。Flow 的思路是把这些动作固化下来素材丢进输入目录一条命令自动完成切片、字幕、封面人只需要处理真正需要判断的部分比如检查字幕断句和标题文案。所以真正省时间的是“批量”和“幂等”。批量指一批视频一次处理幂等指重复运行同一个流程时不会重复生成文件也不会覆盖已经确认过的结果。后面给出的脚本都会围绕这两个特性来写。1.3 适合什么场景不适合什么场景Flow 型影片小工具适合结构化、重复度高、不需要大量人工分镜的任务。典型场景如下场景是否适合 Flow原因直播回放切成短视频适合规则明确按时间区间或静音段切即可口播视频自动加字幕适合语音识别可以批量完成人工只复查课程视频批量转码适合输出格式固定转码参数一致短视频封面批量生成适合尺寸和文案模板固定电影级剪辑、多机位调色不适合需要大量人工判断和艺术调整一镜到底创意视频不适合流程无法预设破坏了创作随机性如果你发现自己的任务几乎每次都要改参数、改规则说明它还没到可以被 Flow 化的程度。先别急着搭工具把手动流程跑顺再抽象。2. 动手前先拆流程四个节点对应四类小工具2.1 素材入库节点所有 Flow 都应该有一个干净的输入层。不要把拍摄视频直接丢给切片命令而是先统一命名、复制到工作目录、生成一份清单。这个清单是后续所有节点的“流转单”里面记录每个视频的源路径、文件名、时长、输出状态。建议的目录结构如下video-flow/ ├── input/ # 原始素材只读 ├── work/ # 中间文件切片、字幕、封面 │ ├── clips/ │ ├── subtitles/ │ └── covers/ ├── output/ # 最终成片 ├── manifests/ │ └── manifest.json # 所有节点的执行记录 ├── tools/ │ ├── ingest.py │ ├── cut.py │ ├── subtitle.py │ └── cover.py └── Makefileinput 目录只存放原始素材不生成任何中间文件work 目录存放切片、字幕、封面output 目录只放最终成片。这样就算中途跑错也不会污染原始素材。2.2 自动切片节点切片的输入是 manifest 中“准备处理”的视频输出是多个片段。切片方式有两种按时间区间切适合已经知道每个片段起止点比如直播回放的章节。按静音检测切适合口播视频去除空白段。ffmpeg 自带 silencedetect可以输出静音开始和结束的时间。切片节点要解决的核心问题不是“能不能切”而是“切在哪里”。静音检测参数如果设置得太灵敏会把正常呼吸停顿当成静音设置得太宽松又会保留很多空白段。这个参数没有万能值需要根据口播节奏调。2.3 字幕生成节点字幕节点负责把音频转成文字。需要选择模型和语言生成 SRT 文件。如果视频最后要发布到没有外挂字幕的平台可以走 ffmpeg 烧录字幕如果平台支持字幕文件就不要烧录保留原视频和 SRT 两个文件画质更好也方便后期修词。字幕节点最常见的坑不是识别率低而是时间轴和断句。断句不对读者看起来会很累时间轴偏移字幕和声音对不上。所以字幕节点生成的不应该只是纯文本而是带时间信息的 SRT。2.4 封面与转码节点封面节点用固定模板生成横版和竖版封面适合批量发布。转码节点负责统一编码和重新封装比如把素材压成 H.264 AAC 的快传格式或者按平台要求生成 16:9 和 9:16 两个版本。封面节点必须考虑字体和文字溢出。很多人第一次写封面脚本时只用默认字体结果中文全是方框或者标题太长直接超出画面。这些问题可以在生成时通过换行和文字宽度计算来规避。2.5 为什么每个节点必须是独立小工具如果四个功能写在一个大脚本里改一处可能影响全部。独立小工具的好处是每个节点都有明确输入输出可以单独测试。某个节点失败只需要重跑该节点。后续可以替换实现比如把本地语音识别换成 API不影响切片和封面。新项目可以复用同一个切片脚本只改参数。也就是说Flow 的价值不在于“一个脚本干完所有事”而在于“每个小工具都能被独立替换、独立调试、独立扩展”。3. 搭建最小可运行的 Flow 视频小工具链3.1 环境准备下面以 Ubuntu / macOS 终端为例。Windows 可以装 WSL2或者用 Git Bash但路径和字体路径要改。mkdir -p video-flow/{input,work/clips,work/subtitles,work/covers,output,manifests,tools} cd video-flow python3 -m venv .venv source .venv/bin/activate pip install faster-whisper pillowffmpeg 用系统包管理器安装# Debian/Ubuntu sudo apt install ffmpeg # macOS brew install ffmpeg检查安装ffmpeg -version | head -n 1 ffprobe -version | head -n 1到这里学习环境已经够用。如果你的视频素材放在 Google Drive 或其他网盘后续可以加一步同步。如果在 Colab 上运行要先把 runtime 切到 GPU否则语音识别会比较慢。3.2 素材入库脚本tools/ingest.py负责扫描 input 目录读取每段素材的时长并把信息写入 manifest.jsonimport json import pathlib import subprocess import sys INPUT_DIR pathlib.Path(input) MANIFEST_PATH pathlib.Path(manifests/manifest.json) ALLOWED {.mp4, .mov, .mkv, .avi} def duration(path): cmd [ffprobe, -v, error, -show_entries, formatduration, -of, defaultnoprint_wrappers1:nokey1, str(path)] out subprocess.check_output(cmd, universal_newlinesTrue).strip() return float(out) def main(): files [p for p in INPUT_DIR.rglob(*) if p.suffix.lower() in ALLOWED] records [] for f in files: records.append({ source: str(f), filename: f.stem, suffix: f.suffix, duration: duration(f), clips: [], subtitle: None, cover: None, status: ready, }) MANIFEST_PATH.write_text(json.dumps(records, ensure_asciiFalse, indent2), encodingutf-8) print(fingested {len(records)} videos) if __name__ __main__: main()这里用 ffprobe 读时长是为了后续切片时能始终知道整段视频的结束时间。manifest.json 是所有节点的唯一数据源。入库脚本应该尽量轻量不要做太多业务判断。3.3 自动切片脚本tools/cut.py调用 ffmpeg 的 silencedetect 检测静音再把静音区间外的内容切出来import json import pathlib import re import subprocess MANIFEST_PATH pathlib.Path(manifests/manifest.json) WORK_CLIPS pathlib.Path(work/clips) def detect_silence(video, noise-30, duration0.5): cmd [ffmpeg, -i, str(video), -af, fsilencedetectnoise{noise}dB:d{duration}, -f, null, -] result subprocess.run(cmd, capture_outputTrue, textTrue) log result.stderr pattern re.compile(rsilence_(start|end): ([0-9.])) events [] for match in pattern.finditer(log): events.append((match.group(1), float(match.group(2)))) ranges [] for i in range(0, len(events) - 1, 2): start events[i][1] end events[i 1][1] if end - start 0.8: ranges.append((start, end)) return ranges def main(): records json.loads(MANIFEST_PATH.read_text(encodingutf-8)) for rec in records: video pathlib.Path(rec[source]) silences detect_silence(video) clip_start 0.0 clip_index 0 for start, end in silences: if start - clip_start 3.0: out WORK_CLIPS / f{video.stem}_clip{clip_index:02d}.mp4 subprocess.run([ ffmpeg, -y, -i, str(video), -ss, str(clip_start), -to, str(start), -c, copy, str(out) ], checkTrue) rec[clips].append({start: clip_start, end: start, file: str(out)}) clip_index 1 clip_start end if rec[duration] - clip_start 3.0: out WORK_CLIPS / f{video.stem}_clip{clip_index:02d}.mp4 subprocess.run([ ffmpeg, -y, -i, str(video), -ss, str(clip_start), -to, str(rec[duration]), -c, copy, str(out) ], checkTrue) rec[clips].append({start: clip_start, end: rec[duration], file: str(out)}) rec[status] cut MANIFEST_PATH.write_text(json.dumps(records, ensure_asciiFalse, indent2), encodingutf-8) if __name__ __main__: main()这段脚本用了-c copy速度很快但时间精度依赖关键帧。素材每隔一两秒一个关键帧时问题不大如果关键帧间隔很大切片点会被拉到最近的关键帧需要重新编码才能精确定位。生产环境建议根据素材实际情况决定是否去掉-c copy。3.4 字幕生成脚本tools/subtitle.py使用 faster-whisper 对每个切片生成 SRTimport json import pathlib from faster_whisper import WhisperModel MANIFEST_PATH pathlib.Path(manifests/manifest.json) WORK_SUBS pathlib.Path(work/subtitles) MODEL_SIZE small DEVICE cpu COMPUTE_TYPE int8 def format_ts(seconds): ms int((seconds % 1) * 1000) s int(seconds) h s // 3600 m (s % 3600) // 60 s s % 60 return f{h:02d}:{m:02d}:{s:02d},{ms:03d} def to_srt(segments): lines [] index 1 for segment in segments: start segment.start end segment.end text segment.text.strip() lines.append(str(index)) lines.append(f{format_ts(start)} -- {format_ts(end)}) lines.append(text) lines.append() index 1 return \n.join(lines) def main(): model WhisperModel(MODEL_SIZE, deviceDEVICE, compute_typeCOMPUTE_TYPE) records json.loads(MANIFEST_PATH.read_text(encodingutf-8)) for rec in records: if not rec[clips]: continue for clip in rec[clips]: clip_path pathlib.Path(clip[file]) segments, info model.transcribe(str(clip_path), languagezh) srt_text to_srt(segments) out WORK_SUBS / f{clip_path.stem}.srt out.write_text(srt_text, encodingutf-8) clip[subtitle] str(out) MANIFEST_PATH.write_text(json.dumps(records, ensure_asciiFalse, indent2), encodingutf-8) if __name__ __main__: main()languagezh只适合中文素材如果素材中有大量英文需要去掉这个参数让模型自动识别。模型首次运行会下载到本地目录之后可以离线使用。CPU 环境建议用 small 加 int8GPU 环境可以换 medium 或 large-v3 加 float16。3.5 封面生成脚本tools/cover.py用 Pillow 生成横版和竖版封面import json import pathlib from PIL import Image, ImageDraw, ImageFont MANIFEST_PATH pathlib.Path(manifests/manifest.json) WORK_COVERS pathlib.Path(work/covers) SIZES {landscape: (1920, 1080), portrait: (1080, 1920)} def make_cover(title, bg(24, 24, 24), fg(255, 255, 255), size(1920, 1080), font_pathNone): img Image.new(RGB, size, bg) draw ImageDraw.Draw(img) font ImageFont.load_default() if font_path: font ImageFont.truetype(font_path, 60) lines title.split(\n)[:3] y size[1] // 2 - len(lines) * 40 // 2 for line in lines: text_width draw.textlength(line, fontfont) x (size[0] - text_width) // 2 draw.text((x, y), line, fontfont, fillfg) y 80 return img def main(): records json.loads(MANIFEST_PATH.read_text(encodingutf-8)) for rec in records: for kind, size in SIZES.items(): title rec[filename].replace(_, ) out WORK_COVERS / f{rec[filename]}_{kind}.jpg img make_cover(title, sizesize) img.save(out, quality85) rec[fcover_{kind}] str(out) MANIFEST_PATH.write_text(json.dumps(records, ensure_asciiFalse, indent2), encodingutf-8) if __name__ __main__: main()字体文件的位置因系统而异macOS 常见/System/Library/Fonts/PingFang.ttcWindows 常见C:/Windows/Fonts/msyh.ttcLinux 可以安装fonts-noto-cjk后使用 NotoSansCJK。如果image.load_default()显示中文乱码就必须传入字体路径。3.6 用 Makefile 串起来为了让整套流程一条命令执行可以在项目根目录写一个 Makefile.PHONY: prepare cut subtitle cover all prepare: python tools/ingest.py cut: prepare python tools/cut.py subtitle: cut python tools/subtitle.py cover: prepare python tools/cover.py all: prepare cut subtitle cover这样执行make all会依次走完素材入库、切片、字幕、封面四个阶段。每个阶段只依赖前一个阶段产生的 manifest所以想单独跑切片也可以先手动执行make prepare再python tools/cut.py。4. 核心参数和原理为什么这样配置4.1 silencedetect 的参数要多试几次ffmpeg 的静音检测主要看两个参数noise和duration。参数含义常用值影响noise静音判定阈值单位 dB-30值越大越容易把环境声当静音值越小越难检测到静音duration连续静音最短时长0.5太短会误切正常呼吸停顿太长会漏切真正空白段建议先拿一段 3 分钟的视频测试打印出所有检测到的静音区间再决定阈值。直播回放通常环境音稳定noise-35比较合适室内口播通常-30左右即可。不要照抄网上的参数不同麦克风差距很大。4.2 faster-whisper 的模型和精度取舍语音识别速度和质量直接受模型影响。下表是常见组合参数含义推荐值说明MODEL_SIZE模型大小small / mediumtiny 快但错字多medium 慢但更准确DEVICE运行设备cpu / cuda学习环境用 cpu生产环境优先 cudaCOMPUTE_TYPE计算精度int8 / float16CPU 用 int8GPU 用 float16language语言zh / en混合语音可以留空自动检测如果字幕错字率太高先换 medium 而不是直接上 large-v3因为 large-v3 在 CPU 上可能慢到无法接受。批量处理时建议每段视频单独转写这样单个视频失败不会连累整批。4.3 封面尺寸和文字安全区多平台封面尺寸差别不大但文字安全区很关键。横版 1920x1080 在视频播放器里会被上下遮挡一部分竖版 1080x1920 在列表页会被裁掉左右两边。安全区通常建议留出 10% 边距也就是文字不要超过中心 80% 的区域。平台横版竖版安全区建议视频号1920x10801080x1920上下留 10%短视频信息流1920x10801080x1920中心 80%封面预览图1280x7201080x1440中心 75%脚本里通常用标题换行来避免溢出最多显示三行。超过三行时应该截断而不是缩小字号否则小屏设备看不清。4.4 manifest.json 是节点之间的契约manifest.json 的结构会直接影响所有节点。下面是一个简化示例{ source: input/口播第01期.mp4, filename: 口播第01期, duration: 610.5, clips: [ { start: 0.0, end: 8.2, file: work/clips/口播第01期_clip00.mp4, subtitle: work/subtitles/口播第01期_clip00.srt } ], cover_landscape: work/covers/口播第01期_landscape.jpg, cover_portrait: work/covers/口播第01期_portrait.jpg, status: done }所有节点不直接互相调用而是通过这个文件交换信息。这样替换字幕模型、修改封面尺寸都不需要动切片脚本。生产环境里manifest 还要加上uuid、created_at、