按 PPT 目录切分视频
把一段(或几段)录屏视频,按 PPT 的目录章节切成独立片段。默认无损流拷贝:
画质、码率、音轨与原片逐比特一致,1 小时视频切片耗时约 10 秒。
判据是文字,不是画面。低于 500 kbps 的录屏(尤其 WPS 演示投屏)做感知哈希
比对完全不可用,实测最高相似度仅 0.72 且无区分度。原因与实证见
references/pipeline.md——动参数前先读它。
硬性红线
- 先探测,再切割。 录屏开头可能有很长的黑屏死气(实测有一段 57 分 38 秒
全黑 + 数字静音)。不先跑probe/frames就切,第一个片段会带着几十分钟黑屏。 - 不凭猜测下结论。 两次观测矛盾时,直接抽该时刻的帧实测比对,不要推断。
- 如实报精度。 做了
refine边界精度约 ±1 秒;跳过则约 ±30 秒。
章节是否分对取决于anchor_pages是否正确。做不到就说明做不到。 - 模型不能读图时不要假装读过。 全部校验走 OCR 文本 + 数值。
- 不重新编码,除非用户明确要求。流拷贝是"直接视频生成视频"的正解。
- 片段只会略长,不会丢内容(
-ss落在前面的关键帧上)。校验时长时按
max(5s, 2%)容差判断,不要为几秒偏差反复重切。 - 不篡改原视频。 只读原片,输出到新目录。
工作流
全程用一个 --work 目录保存中间产物,每一步都可重跑,不必一次跑完。
0. 环境
- 调
load_workspace_dependencies取 Python 路径。 ffmpeg在 Windows 上通常没装。先探测,缺了就下:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force # 脚本签名策略
& <skill-dir>\scripts\get_ffmpeg.ps1 -Work <work>
后续所有 Python 调用形如:
$py = "<load_workspace_dependencies 返回的 python>"
$vs = "<skill-dir>\scripts\vsplit.py"
$env:PYTHONIOENCODING = "utf-8" # 中文输出
& $py $vs <子命令> ...
1. probe — 视频体检
& $py $vs probe --video "<视频>" --work <work>
输出时长、流、是否自带章节、关键帧表(median GOP / max GOP)、格式元数据。
如果 chapters: N found 且章节名就是想要的章节——直接把章节时间写进
boundaries.json 的 cuts({deck,no,title,start,end,dur}),跳到第 8 步 cut,
不必走下面的 OCR 流程。
2. outline — 读 PPT 目录
& $py $vs outline --work <work> --deck "DMS:<DMS.pptx>" --deck "APM:<APM.pptx>"
--deck NAME:PATH可重复;顺序必须等于视频里的播放顺序。- 输出
outline.json:每页的文本行、标题,以及自动探测的目录页与章节提议。 - 目录页是按结构认出来的:整页由
编号/章节名成对组成,而不是看有没有
"目录"两个字——真实目录页上常常只有01 服务介绍 02 信息调研 …,
反倒有正文页标题叫「专业服务产品目录」会被误命中。
重复出现的目录页(每章开头各一份)会被一并识别,它们是很好的章节锚点。
3. sections — 产出并复核 sections.json
& $py $vs sections --work <work>
生成 sections.json 后打开看一眼。每个章节:
| 字段 | 含义 |
|---|---|
anchor_pages |
该章首页在 PPT 里的页码。视频里这一页首次出现 = 该章开始 |
lead_pages |
可选。章节前的目录/分隔页页码,用于把起点往前挪到分隔页 |
脚本会打印一张复核表,逐条看这三类提示:
anchor=MISSING—— 没找到锚点页,必须手工指定。<- slides N look like this section's lead-in—— 这条提示可靠。说明第 N 页
对本章名的贴合度高于对上一章名。典型情况:目录写"安装部署",正文首页标题却是
"环境准备"。把anchor_pages改成[N]。hint: slides 1-2 (deck title / agenda) precede the first section—— 每份 PPT 的
第一章默认锚在标题页(第 1 页),这样开场与目录页属于第一章。
想从正文首页开始就把anchor_pages改成目录页 +1。
不确定页码时用 outline.json 里的 slides[].text 对照。
> sections 只是提议,match 之后还要再复核一次。 常见情况:提案的锚点是
> 章节分隔页,但讲师把分隔页直接点过去了(只停 1 秒),视频里根本没留下——
> 于是那一章整个找不到边界。这时把它改成该章的第一个正文页(目录页的下一页)。
> 反过来,某章算出 8 秒、13 秒这种极短时长,先别怀疑算错:很可能讲师真的跳过了,
> 如实保留并在索引里说明。
4. frames — 抽帧、找稳定状态、选代表帧
& $py $vs frames --video "<视频>" --work <work>
1 fps 抽帧 → 亮度图(报出黑屏死气区段,并停下让你确认要不要排除)→
帧间差分找稳定画面状态 → 每个状态全分辨率重抽一张代表帧到 reps/。
黑屏提示会打印 !! blank (near-black) stretches found。看到就停下来问用户:
是排除还是保留。
5. OCR 代表帧
& $py $vs ocr-list --work <work> # 打印现成命令
$files = (Import-Csv <work>\rep_manifest.csv | ForEach-Object { $_.file })
& <skill-dir>\scripts\ocr.ps1 -Paths $files | Out-File <work>\ocr_reps.txt -Encoding UTF8
ocr.ps1 用 Windows 内置 Windows.Media.Ocr(无需安装任何东西),
按传入的完整路径逐图输出 ===== <path> ===== 块。日志很大(1 小时视频约 190 KB),
不要让 OCR 文本进你的上下文,交给 match 读。
6. match — 文字匹配 + Viterbi → 候选边界
& $py $vs match --work <work>
输出 timeline.json(逐帧幻灯片时间轴)与 boundaries.json(各章起止),
并在终端打印合并后的时间段视图(from / to / score / slide)和候选切点表。
看两件事:
- 时间轴里若有明显不合理的跳跃(比如 APM 段一直标成 DMS 末页),
参考references/pipeline.md第 3 节的转移代价,不要乱调阈值。 !! sections without a boundary表示某章锚点页在视频里没找到——
多半是那页被跳过了,或anchor_pages指错了。
此时必须停下来把候选边界给用户看,确认后再做 refine。另外若打印了
!! N s (… -> …) before the first section are covered by no clip,说明开头那段
非 PPT 画面(投屏/门户/桌面)没归入任何章节,要和用户确认留还是切出来。
7. refine + 再 OCR + match --fine — 把边界收到 1 秒内
& $py $vs refine --video "<视频>" --work <work>
$files = (Import-Csv <work>\fine_manifest.csv | ForEach-Object { $_.file })
& <skill-dir>\scripts\ocr.ps1 -Paths $files | Out-File <work>\ocr_fine.txt -Encoding UTF8
& $py $vs match --work <work> --fine
refine 一次采两个尺度:大范围 3 秒步长找对邻域,候选点附近再 1 秒步长精修。
实测一份 11 章的 1 小时视频共取 825 帧、约 3 分钟。
跳过这一步精度约 ±30 秒(受代表帧位置支配);做了约 ±1 秒。要精确就做。
> refine 是逐时间戳单独定位的,不是用 fps 滤镜批量抽。
> fps 滤镜会把帧吸附到自己的输出网格上,与真实时间戳有 1–2 秒的静默偏移,
> 足以让所有边界一起偏。详见 references/pipeline.md。
8. cut — 流拷贝出片 + 校验
& $py $vs cut --video "<视频>" --work <work> --out <work>\clips
-c copy 无损切割,切点自动回吸到 --snap(默认 4 秒)内的关键帧,
随后自动 ffprobe 校验每段的时长与流数量并打印结果。
若某章算出 ≤0 秒,脚本会报错并跳过而不是照切——这说明锚点顺序错了,
回去看 sections.json。
> ⚠️ DSH 沙箱:被 spawn 的进程(ffmpeg)只能写工作区内,写工作区外报
> Permission denied,而 PowerShell 自己的 Copy-Item 能写任意位置。
> 所以先切到 <work>\clips,再用 Copy-Item 搬到目标目录。
> 不要为这个去查 ACL,那是预期行为。
9. verify — 确认每段开场是对的章节页
& $py $vs verify --work <work>
$files = (Import-Csv <work>\clipcheck_manifest.csv | ForEach-Object { $_.file })
& <skill-dir>\scripts\ocr.ps1 -Paths $files | Out-File <work>\ocr_clipcheck.txt -Encoding UTF8
把 OCR 结果与章节首页文字对一遍。全部对得上再交付。
10. 交付
Copy-Item "<work>\clips\*.mp4" "<目标目录>" -Force
用 present 交付片段 + 一份索引说明(Markdown 或 txt),索引里写清:
- 每个片段对应的目录章节、起止时间码、时长、大小;
- 视频里的异常发现(黑屏死气、非 PPT 演示段、被跳过的章节);
- 分界是怎么定出来的、精度多少、哪些地方需要人工确认。
不要交付中间脚本与抽帧素材。
常用参数
| 场景 | 做法 |
|---|---|
| 视频自带章节 | probe 里读出章节起止,手写进 boundaries.json 的 cuts,直接 cut,跳过 OCR 全流程 |
| 用户直接给了时间码 | 同上:手写 cuts 后跑 cut,不需要 PPT 和 OCR |
| 只要粗切、不要精细 | 跳过 refine,边界精度约 ±30 秒 |
| 只关心某一段 | frames --lo <秒> --hi <秒> 限制范围 |
| 私有构造,不连外网 | 先手工放一份 ffmpeg 到 <work>\tools\,或 --ffmpeg <路径> |
| 章节很短(讲师跳页) | 如实保留,在索引里说明原因,不要合并掉 |
边界
- 章节划分依赖人工确认的
anchor_pages。脚本能自动提议,但目录名与正文页
标题不一致时必然提错,必须看一眼sections.json。 - 两份 PPT 的目录页文字可能完全相同(只有页码不同),此时只能靠播放顺序 +
Viterbi 单调性区分,无法靠单帧文字区分。 - 低码率画面 OCR 噪声大,正确匹配的得分也常见 0.40–0.70,
阈值默认 0.60 而非常识上的 0.9。不要为了提高"把握"盲目提高阈值,会把正确页筛掉。 - 需要视觉核查时必须走
office-pptxskill 的渲染 CLI(DSH 捆绑的 LibreOfficeKit),
不要去找系统安装的soffice。模型不能读图时如实报告"视觉核查未完成"。 - OCR 只支持 Windows(
Windows.Media.Ocr)。非 Windows 需自行替换 OCR 后端,
匹配与切割部分不受影响。 - 本 skill 只做按章节切分,不做转码、压缩、拼接、字幕或画质修复。