视频剪辑 是做什么的?
将一段未经剪辑的长视频转化为紧凑、可观看的初剪——果断、内容感知且自我验证。人类(你,模型)负责做出“保留什么”的编辑决策;附带的脚本负责机械性的精确操作(边界对齐、静音回收、内存安全渲染、自检)。
使用时机
- 需要将原始/未剪辑的视频(YouTube URL 或本地文件)变得更紧凑。
- 要求是“将其减半”、“剪掉无聊的部分”、“制作一个初剪/粗剪”。
- 不适用于:逐字的填充词微调整、多机位B-roll组装、颜色/音频润色——这些属于后续的P1/P2精修环节。此技能是P0:验证分析结果 + 产生一个可靠的剪辑。
依赖项
/video-understand 是先决条件。请先运行它,并使用其验证后的媒体、转录文本、分析结果和时间线,而不是在此技能中重新创建。在开始之前,请验证它是否已安装。如果未安装,请警告用户缺少此先决条件,并在处理媒体前停止。
激活任何包含以下依赖项的环境(不要假定特定的 conda 环境名称):
yt-dlp(下载),ffmpeg/ffprobe(剪辑/渲染/探测)位于 PATH 上- 带有
faster-whisper的 Python(CPU 可用:device=cpu, compute_type=int8) 首先检查:yt-dlp --version,ffmpeg -version,python -c "import faster_whisper"。
项目协议工作流程
使用以下持久化项目文件:
work/cut/edit-plan.json # 仅手工编写的保留/丢弃决策
work/timeline.json # 生成的精确范围 + 素材/节目映射
review/01-cut/cut-summary.md
review/01-cut/timeline-map.png
review/01-cut/boundary-review.mp4
review/01-cut/full-proxy.mp4 # 可选的整节目节奏审查
每个规范决策具有稳定的 id、action(keep 或 drop)、start_s、end_s、reason 以及语义/转录 evidence_refs。保持决策按时间顺序;V1 不支持重新排序或重复的源素材片段。
重用共享的证据层,而不是重新转录或诊断:
python skills/video-understand/scripts/transcribe.py work/cache/audio16k.wav work/understand/transcript
python skills/video-understand/scripts/analyze.py work/understand/transcript.json work/understand/analysis.json
现有的精确脚本接受规范计划。扩展单词安全的边界,可选地分配线性变速,然后生成共享时间线:
python skills/video-cut/scripts/build_edit.py work/cut/edit-plan.json work/understand/transcript.json work/cache/edit-final.json
python skills/video-cut/scripts/assign_speed.py work/cache/edit-final.json work/understand/transcript.json
python skills/video-understand/scripts/build_timeline.py work/cache/edit-final.json work/timeline.json --fps-num SOURCE_FPS_NUM --fps-den SOURCE_FPS_DEN
使用来自 work/understand/media.json 的确切有理数 FPS;从不将 30000/1001 舍入为 30/1。审查 timeline-map.png 和简短的 boundary-review.mp4。仅在必须审查整节目节奏时生成 full-proxy.mp4。将结果写入 cut-summary.md,并将操作检查报告指向该文件。
记录时间线贡献及其所需的源输入,更新操作整数 revision,并将 based_on 设置为所消耗的确切理解修订版本:
"render": {
"kind": "timeline-transform",
"input": "../input/original-video.mp4",
"plan": "timeline.json"
}
在所有修订检查通过后,共享交付渲染器对活动序列进行编码。
独立兼容性工作流程
以下旧版文件和命令在迁移期间仍然受支持。build_edit.py 同时接受 edit_coarse.json 和规范的 decisions[];cut_render.py 同时接受 edit_final.json 和 timeline.json;此技能的 transcribe.py 是围绕 ../video-understand/scripts/transcribe.py 的兼容性包装。
将所有中间文件保存在 work/ 下,交付到项目根目录:
work/source.mp4 # 下载的原始文件
work/audio16k.wav # 用于 whisper 的 16k 单声道音频
work/transcript.json/.srt # 单词级别的时间戳
work/analysis.json # 诊断(静音、填充词、连续讲话)
work/edit_coarse.json # *** 你的编辑决策(手写) ***
work/edit_final.json # 扩展的紧凑渲染片段(生成;包含每个片段的速度)
work/selfcheck/ # 剪辑重新转录 + 帧连接检查
first_cut.mp4 # 可交付产物
first_cut_report.md # 编辑 + 自检报告
工作流程(6 步)
1. 下载
最高可访问质量 + 音频。默认播放器客户端经常失败(不可用/DRM/登录要求)。可靠的备选方案是 android 客户端:
yt-dlp --extractor-args "youtube:player_client=android" \
-o work/source.mp4 "<URL>"
如果无需登录只能获取混合的 360p(格式 18),请接受它并记录此限制——HD DASH 经常受到 DRM/登录保护。然后提取音频:
ffmpeg -y -i work/source.mp4 -ac 1 -ar 16000 work/audio16k.wav
2. 转录(单词级别)
python scripts/transcribe.py work/audio16k.wav work/transcript [base.en]
生成 work/transcript.json(片段 + 每个单词的开始/结束时间)和 .srt。
使用 faster-whisper, CPU/int8, VAD 开启, word_timestamps=True。
非英语: 传递一个多语言模型 + --lang,例如
python scripts/transcribe.py work/audio16k.wav work/transcript medium --lang zh
(.en 模型仅支持英语)。先在一个短样本上验证语言。
注意: Whisper 会去除大多数发声的“嗯/呃”——它们无法通过转录测量,因此填充词检测必须将下面的词汇搜索与静音间隙分析相结合,而不是仅依赖转录文本。
3. 诊断(“看” + “读”)
python scripts/analyze.py work/transcript.json work/analysis.json
揭示:开头的静音、结尾的静音、单词间静音 ≥0.8 秒(以及它们跟随的单词)、填充词计数、wpm、讲话比例、近似重复、长度 >40 秒的独白片段(可能的闲聊候选)。
要看画面,每约 2 秒采样一帧形成联系表,并亲自阅读;确认场景结构/查找黑色或损坏帧:
ffmpeg -y -i work/source.mp4 -vf "fps=1/2,scale=240:-1,tile=10x10" -frames:v 1 work/contact.png
ffmpeg -i work/source.mp4 -vf "blackdetect=d=0.1:pic_th=0.98" -an -f null -
对于单个静态的说话头像镜头,没有场景切换——预期如此,并注意后续的跳跃剪辑是正常的,而不是故障。
4. 决定剪辑——手写 edit_coarse.json
这是编辑核心。阅读转录文本 + 分析结果,然后编写一个保留块和丢弃跨度的粗略计划,这些块和跨度无间隙/无重叠地平铺整个时间线,每个块/跨度都有 in、out 和一行 reason。将缓慢的前言压缩为少数高价值的精华部分;保留主旨内容为较大的块;丢弃明显的离题/闲聊。保持时间顺序;规范的 V1 时间线拒绝重新排序。目标是大约将运行时间减半。
- 格式规范 + 一个真实的工作示例:
reference/edit_coarse.schema.md。 - 将每个块的边界对齐到句子/小句的结束——绝不在句子中间或口吃处开始/停止。使用:
它接受传统python scripts/inspect_bounds.py work/edit_coarse.json work/transcript.jsonkeep[]和规范decisions[]计划,并打印跨越每个边界的单词,以便你可以将它们调整到干净的边界上。
5. 扩展 → 自动变速 → 渲染
将粗略块转换为紧凑、无静音的渲染片段,标准化讲话速度,然后渲染:
python scripts/build_edit.py work/edit_coarse.json work/transcript.json work/edit_final.json 1.5
python scripts/assign_speed.py work/edit_final.json work/transcript.json # 添加每个片段的速度
python scripts/cut_render.py work/edit_final.json work/source.mp4 first_cut.mp4
build_edit.py在大于SPLIT_GAP(第 4 个参数,例如 1.5 秒)的内部暂停处拆分每个块,并将每个边界修剪到静音内部(前导 0.10 秒 / 尾部 0.30 秒),这样就不会剪掉任何单词,同时回收静音时间。较低的SPLIT_GAP= 更紧凑但更跳跃;对于语速慢、深思熟虑的说话者,建议使用 1.5 而不是 0.6(避免过于琐碎的微小剪辑)。assign_speed.py(自动变速)——按decision_ref分组渲染片段,测量每个手写保留决策的讲话速度,并将一个共享的speed因子以及每个片段产生的out_dur写回edit_final.json。速度已在舒适范围内的保持在 1.0;超出范围的速度仅移动到最近的死区边界。相邻决策块之间的差异不超过 0.08 倍,以避免“抽动”式的连接。它仅改变速度——从不改变保留的内容。在渲染前阅读打印的表格:- 指标:单词/分钟(英语)——对于 CJK(中文/日语/韩语)自动切换为字符/分钟。
- 默认值是首次运行的起点,而非验证过的常数:死区 145–185 WPM,速度限制在 1.0–1.3。这是仅加速:说话慢的决策块可以被加速,而自然快的说话保持 1.0。短的决策块(<3.5 秒 / <6 个单词)噪音太大无法测量,将继承全局因子。
- 速度是在静音回收后测量的,因此比总 WPM 更高,更接近发音速率。打印表格的审查是关键:在渲染前目测并调整
deadband以匹配说话者,不要盲目相信默认值。 - 旋钮(k=v):
mode=segment|global|off(默认segment;global=整个视频一个因子,off=禁用),deadband=lo,hi,min_speed=/max_speed=,min_seg=/min_words=,lang=auto|en|cjk。要完全跳过此功能,只需不运行此步骤——cut_render.py将缺失的speed视为 1.0。
cut_render.py使用 N 个独立的 seeked 输入 + concat 过滤器,单次重新编码。这是强制的:从一个解码输入上 N 次修剪会使 ffmpeg 缓冲整个解码流(对于长视频可达数十 GB)。参见reference/pitfalls.md。 当片段带有speed时,它重新计时每个输入(视频的setpts/ 音频的atempo),并在 concat 之前标准化每个输入的 fps/时间基准/采样率,以便音视频保持锁定;如果没有速度,它回退到原始的确切图形。- 最终比较:
video-edit-compare读取规范时间线加上实际的最终交付产物。它将最终像素投影回源时间,将变速剪辑拉伸到它们的源持续时间,并将丢弃的范围显示为黑色。
6. 自检——在通过此检查之前不要声明完成
# 协议精度回归检查(从仓库根目录运行)
python skills/video-cut/scripts/check_project_protocol.py
# 重新转录完成的剪辑
ffmpeg -y -i first_cut.mp4 -ac 1 -ar 16000 work/selfcheck/cut_audio16k.wav
python scripts/transcribe.py work/selfcheck/cut_audio16k.wav work/selfcheck/cut_transcript
# 内容 + 填充词 + 边界检查
python scripts/verify_cut.py work/edit_final.json work/transcript.json work/selfcheck/cut_transcript.json
# 帧 / 黑色 / 连接检查(连接时间使用变速后的 out_dur,因此保持准确)
python scripts/selfcheck_frames.py work/edit_final.json first_cut.mp4 work/selfcheck
# 音画同步 + 无残留静音
ffprobe -v error -select_streams v:0 -show_entries stream=duration -of csv=p=0 first_cut.mp4
ffprobe -v error -select_streams a:0 -show_entries stream=duration -of csv=p=0 first_cut.mp4
ffmpeg -i first_cut.mp4 -af silencedetect=noise=-32dB:d=1.5 -f null -
通过阈值:
| 检查项 | 通过条件 |
|---|---|
| 黑帧 | 无 |
| 连接帧 | 干净——仅自然头部跳跃,无黑色/冻结/撕裂 |
| 音画同步 | 视频与音频时长差约 1 帧(≈30ms);总时长 ≈ 变速后的 final_duration_s_after_speed |
| 静音 ≥1.5秒 | 0(讲话比例应显著上升) |
| 丢弃/重复的片段 | 无——所有关键内容都存在(difflib ~0.90 可接受;较低比例通常是针对含糊不清讲话的二次转录噪音,而非内容丢失——在归咎于剪辑之前,通过关键词存在来验证) |
| 被切断的半句 | 无——边界落在静音/句子结束处 |
关于填充词的诚实说明: 段落级别的剪辑不会实现“几乎没有填充词”。每分钟的密度甚至可能上升,因为保留的内容是对话性质的。如实报告这一点;单词级别的填充词去除属于 P1/P2(并且会使单次拍摄的讲话头像看起来明显跳跃)。
最后
编写 first_cut_report.md:源摘要、剪切了什么及原因、自检表以及任何诚实的注意事项(来自 360p 源的质量上限、填充词密度等)。如果应用了变速,请透露:重新计时的片段数量及速度范围(来自 assign_speed.py 的表格 / speed_params),以便读者知道剪辑不是 1:1 实时。还要注意,verify_cut.py 的每分钟填充词数字在加速后看起来略高(相同单词,更短的分钟)——这是一个报告伪像,而不是添加了填充词。
关键原则(来之不易的)
- 你决定内容;脚本决定帧。 手写
edit_coarse.json;让build_edit.py做精确,assign_speed.py做速度。 - 速度影响节奏,不影响内容。 自动变速为来自一个
decision_ref的所有渲染片段提供相同的速度,仅将超出范围的节奏移动到最近的死区边界,将相邻决策的差异限制在 0.08 倍以内,并将输出限制在 1.0–1.3。atempo保留音调。审查表格;设置为mode=off或跳过此步骤以禁用。 - 绝不剪掉一个单词。 边界始终落在静音内部。
- 仅内存安全渲染(N 个 seeked 输入 + concat)。绝不对一个输入进行多次修剪。
- 果断决策。 做出合理判断并继续前进;不要过度询问。
- 自检不可妥协。 在声称完成之前重新转录 + 帧检查。
- 参见
reference/pitfalls.md获取陷阱和修复方法的完整列表。