# 会议录屏 PPT 页面提取 MVP 编码方案 V002 创建人员:`dev.developer.project` 文件职责:在 V001 已通过范围上,只闭环 F1 判定互斥/防漂移、F2 FFmpeg 管道与时间/回收、F3 PDF 顺序与单页内存证据。 管理规范/模板:`../../../编码规范.md`;`../../../开发审计规范.md`;`../../../../common/dev-doc/编码规范.md`。 引用文件:`CODE-DESIGN-PROJECT-INFO-MEETING-RECORDING-SLIDE-EXTRACTION-MVP-V001.md`;`../../../开发审计报告.md`;`../../../ai-media-processor/draft/会议录屏PPT页面提取方案_v0.1.md`。 记录方式:V001 的最小后继;只覆盖首次审计 F1—F3,复审 PASS 前不开始实现。 ## 1. 前序结论与冻结边界 - 事项:`DEV-PROJECT-INFO-MEETING-RECORDING-SLIDE-EXTRACTION-MVP-20260801-001`。 - 前序审计:`DEV-AUDIT-PROJECT-INFO-MEETING-RECORDING-SLIDE-EXTRACTION-MVP-DESIGN-20260801-001=HOLD/3`。 - V001 已通过且继续冻结:单视频 CLI;默认/显式输出;Pillow+NumPy+FFmpeg/FFprobe;1 fps 扫描;原分辨率 PNG;一个 PDF;正式目录同级唯一暂存和一次目录重命名;既有输出/符号链接先拒绝;中文路径;无 GPU;不读取真实业务视频。 - 排除项继续冻结:PPTX、OCR、人像处理、画面/PPT 区域裁剪、会议界面移除、JSON、转写、批处理、GUI、API、数据库、Skill 和阈值 CLI。 - 文件仍限于一个脚本、一个测试文件和一页用法,不新增 package、公共模块、服务或额外业务产物。 ## 2. F1:稳定段防漂移与互斥页面分类 ### 2.1 稳定段的双约束 扫描帧以 `ScanFrame(sample_index, relative_seconds, gray_feature, perceptual_hash, content_score, sharpness)` 逐个消费。当前稳定段只保留:不可变 `segment_anchor`、前一帧、样本数、最佳清晰度帧摘要和是否因锚点漂移终止;不保存帧列表。 一个新帧只有同时满足以下条件才进入当前稳定段: 1. 与前一帧的 `mean_abs_diff` 和 `changed_ratio` 都不超过“相邻稳定”常量; 2. 与不可变 `segment_anchor` 的同两项指标都不超过更严格的“段锚点”常量。 `segment_anchor` 在该段内永不滚动。任一条件失败即结束该段并以当前帧建立新锚点;只有至少 `3` 个采样帧(约 2 秒)且从未违反锚点约束的段才产生候选。由相邻小变化累计到锚点超限的慢速渐变段标为 `anchor_drift`,不得把超限帧或锚点到超限之间的中间帧选为该段最佳帧;已在渐变前满足 3 帧的旧稳定平台仍可形成旧页候选。新锚点必须重新满足完整 3 帧,单帧和双帧中间态不产出。 离线固定序列覆盖:稳定 `A` 三帧 → 每帧仅小变但首尾大变的六帧渐变 → 稳定 `B` 三帧。最终候选只能是 `A,B`,不得包含渐变帧。 ### 2.2 内容分数 不使用 OCR。对固定小尺寸灰度特征计算: - `edge_density`:水平/垂直一阶绝对梯度之和达到命名阈值的像素比例; - `contrast`:灰度标准差除以 `255`; - `content_score = edge_density + 0.25 * contrast`。 比较容差冻结为命名常量 `CONTENT_SCORE_TOLERANCE`: - 新分数 `> current + tolerance`:内容增加,采用后一个稳定候选; - 新分数 `< current - tolerance`:内容减少,保留当前较完整状态; - 差值落在容差内:视为完整度相同,采用后一个稳定时间以符合“最后稳定状态”,但不新增页面。 ### 2.3 互斥分类顺序与防跨步漂移 每个确认页保存不可变 `logical_anchor_feature/first_seen_order` 和可更新 `current_feature/current_timestamp/current_score`。候选严格执行以下单一分支顺序: 1. **最近逻辑页动画分支**:先同时比较候选与最近页的 `current_feature`、不可变 `logical_anchor_feature`。只有“与当前状态为小步变化”且“相对逻辑锚点的累计变化仍在上限内”才属于同页动画;然后按内容分数规则替换或保留。不可变锚点防止多次小步合并跨越成另一页。 2. **历史返回去重分支**:只有未命中动画分支时,才与除最近页外的历史页 `current_feature` 做近似重复比较;命中则跳过且不改变首次顺序。 3. **新页分支**:前两者均未命中时追加新逻辑页。 三个动作互斥且每个候选只执行一个。固定测试:`A初始 -> A完整` 必须更新为一页后态;`A -> B -> A` 只保留 `A,B`;内容明显减少不覆盖;同分采用较后时间;候选同时接近最近页和历史页的交叉阈值用例必须优先执行最近页动画,不被全局去重吞掉。 ## 3. F2:受管 FFmpeg、精确读取与时间映射 ### 3.1 统一受管子进程边界 单脚本内只通过两个同源 helper 启动 FFmpeg/FFprobe:流式 rawvideo helper 与无 stdout 数据流的文件输出 helper。共同合同: - 参数数组启动,`shell=False`,stdin=`DEVNULL`;长时 stderr 直接重定向到本次暂存目录的临时文件,不使用未消费的 `PIPE`,失败时只读取有界尾部加入错误消息。 - `BaseException` 清理顺序固定为:关闭 Python 持有的 stdin/stdout 端 → 若子进程仍运行则 `terminate` → 限时 `wait` → 超时则 `kill` 并再次 `wait` → 关闭 stderr 文件句柄 → 清理本次暂存 → 对 `KeyboardInterrupt/SystemExit` 裸 `raise`,普通异常包装为明确工具错误。 - 正常路径必须等待进程结束并检查退出码;非零退出即失败。子进程未被 wait 回收时不得开始删除暂存目录或返回。 ### 3.2 `read_exact` 与 EOF `read_exact(stream, frame_bytes)` 循环读取直至: - 累计正好 `frame_bytes`:返回一帧; - 第一次读取即零字节:返回干净 EOF; - 已有残片后读到零字节:抛出“rawvideo 半帧 EOF”错误。 单次 `read()` 短读不被解释为完整帧。扫描正常 EOF 后仍须关闭 stdout、等待 FFmpeg、检查退出码和 stderr;退出码非零时即使帧数大于零也失败。 ### 3.3 PTS 归一与第二阶段定位 扫描滤镜顺序固定为:`setpts=PTS-STARTPTS,fps=fps=1:start_time=0,scale=:`。因此 rawvideo 第 `n` 个输出样本(从 `0` 开始)的选择时间唯一映射为源视频展示时间基准后的 `n.000` 秒;FFprobe 的非零起始 PTS 不直接加入该相对时间,VFR 由 fps 滤镜在归零时间轴上采样。 第二阶段采用输入级 `-ss `、同一第一视频流映射和转码到 PNG;FFmpeg 默认准确转码寻址会从前一关键帧解码并丢弃至目标相对时间,不使用 `-noaccurate_seek`。提取后严格核对 PNG 宽高等于 FFprobe 原视频宽高。命令构造单元测试冻结 `setpts/fps/start_time=0`、样本 `n -> n 秒` 与 `-ss` 值;合成 VFR/非零 PTS 小视频再验证已知时间颜色帧对应。 ### 3.4 离线恢复测试 - 假流多次短读拼成一帧;干净 EOF;半帧 EOF。 - 子进程先输出足量 stderr 再输出帧,证明 stderr 文件重定向不死锁;非零退出必须失败。 - 扫描阶段与原图提取阶段分别注入 `KeyboardInterrupt`;记录假进程 `terminate/wait/kill` 调用顺序,断言进程已退出、暂存/正式目录不存在、原异常类型不变。 - 普通 FFmpeg 错误断言用户可定位的阶段和有界 stderr 尾部;不读取真实业务视频。 ## 4. F3:PDF 内容顺序与单页存活证据 ### 4.1 writer 内存合同 PDF writer 只把 PNG 路径列表、对象编号和 xref 偏移保存在内存;这些均是按输出页数增长的小型元数据。对每个 PNG 按顺序执行: 1. `with Image.open(path)` 打开当前页并转换 RGB; 2. 取得当前页 raw RGB,立即 zlib 压缩; 3. 立即把本页 Page/Image/Content 对象和压缩 payload 写入暂存 PDF; 4. 在进入下一页前释放 raw RGB、压缩 payload 和 Pillow 图像。 禁止构造 Pillow image 列表、raw payload 列表或压缩 payload 列表。任一时刻最多一个解码后的全分辨率页面和一个对应压缩 payload 存活;PDF 完成并 `fsync` 后才参与正式目录提交。 ### 4.2 确定性内容与顺序核验 脚本写出的每页 Image XObject 固定为 `/DeviceRGB`、`8 bits`、`FlateDecode`,Page 按 `slide_001.png...` 的顺序引用对应对象。测试侧使用只读最小解析 helper 按 Page Kids 顺序读取每页 Image stream、zlib 解压,并逐页核对: - width/height 与对应 PNG 一致; - 解压后的 RGB SHA-256 与对应 PNG 转 RGB 的 SHA-256 一致; - 第 1/2 页分别为合成视频的 `A完整/B` 唯一颜色块与像素标记。 同时保留 `pdfinfo` 可打开和页数检查,从而共同证明非空白、内容一致、顺序一致。 ### 4.3 多页单页存活测试 PDF writer 的图像打开入口允许测试注入等价 opener。受控多页测试提供至少 `20` 个不同页面,并用 context-manager 计数器记录解码图像进入/退出;断言: - `max_active_decoded_images == 1`; - 下一页进入前上一页已退出; - 最终 PDF 20 页且逐页 RGB 哈希按输入顺序一致。 该证据只证明全分辨率页面不累计,不新增真实视频、复杂 RSS 矩阵或业务 JSON。 ## 5. V002 复审与放行 ### 5.1 已授权真实验收样本(不改变实现合同) 新增消息 `msg_20260801144622354_1c75b06b` 授权只读使用: `G:\熊猫财经\20260729192357-2026小郑掘金产业链之算力行业付费直播-视频-1-共享屏幕.mp4` 请求方已只读核对:H.264、`2560x1440`、`12866.880 s`、`1268392019 bytes`。本追加只改变实现后的验收顺序,不改变 F1—F3、算法阈值或产品输出: 1. V002 复审 PASS、离线测试和合成视频 CLI 全部通过前,不读取或截取该视频。 2. 基本实现稳定后,先记录原视频 `size/CreationTimeUtc/LastWriteTimeUtc/SHA-256`。如需调试,只在 `dev/project-dev/tmp/DEV-PROJECT-INFO-MEETING-RECORDING-SLIDE-EXTRACTION-MVP-20260801-001/` 用 FFmpeg 从原视频只读生成一个独立短片副本;记录起止时间、命令、短片大小/哈希,不在原目录写任何文件。 3. 短片若暴露具体阈值问题,只围绕单一证据最小修复并回归;避免在已知失败下反复运行 3.5 小时完整视频。 4. 离线/短片结果稳定后运行一次完整视频,输出和资源证据仍只写上述任务临时根。记录退出码、墙钟耗时、外部采样的 Python+FFmpeg 峰值工作集、PNG 数量/尺寸、PDF 页数/内容顺序、动画/重复/过渡抽查和暂存残留。 5. 全部动作结束后再次记录原视频四项指纹并逐项相等;人像/会议界面保留不是失败。完整媒体与输出保留到 `case_analysis.media_processor` 只读复核后再决定清理。 峰值内存采用测试执行层的只读外部采样,不进入产品 CLI、不增加 JSON 产品输出或常驻监控模块。 | 前序 finding | V002 闭环 | 实现测试门禁 | |---|---|---| | F1 相邻稳定漂移、动画/去重冲突、内容分数未定义 | 第 2 节 | 锚点渐变、互斥优先级、增加/减少/同分、交叉阈值 | | F2 短读/EOF/stderr/退出码、时间映射、孤儿进程 | 第 3 节 | read_exact、半帧、非零/大 stderr、n→秒、VFR/非零 PTS、中断回收 | | F3 PDF 只证页数、未证顺序/内存 | 第 4 节 | Page 顺序 RGB 哈希、pdfinfo、20 页 opener 最大存活 1 | V002 复审只核对 F1—F3、已授权真实验收顺序和 V001 已通过边界是否回退;PASS 后才创建实现、测试和用法。真实视频只在离线基本实现稳定后按第 5.1 节执行。 ## 6. 当前状态 `PENDING_INDEPENDENT_DESIGN_REREVIEW`