{
  "schemaVersion": 1,
  "batchId": "pseo-batch2-20261014",
  "blueprintId": "blueprint-30",
  "kind": "recipe",
  "slug": "frame-extraction-timebase-validation",
  "locale": "zh",
  "evidenceClass": "fixture-run",
  "sourcePath": "content/pseo/batch2-20261014/recipes/frame-extraction-timebase-validation.json",
  "sourceHash": "e161b2930a717bac745093a4145abfb58220c31b7ae451f10d3706066731733a",
  "contentHash": "12f512f218c3b4da415fdc238be2c9decb16ce708aa30ad69d2d4394ee47eb41",
  "contentVersion": "authored-12f512f218c3b4da415fdc23",
  "rendererHash": "727572c29f88632162b30bf97d95dc1be416eb61ca002023ad1c1b1410d29fbd",
  "copy": {
    "title": "按时间基准抽帧：CFR、VFR 与精确帧索引验收",
    "description": "下载 Python 与 FFmpeg 抽帧配方，运行原创 CFR/VFR 夹具，查看有理数 PTS 表、三种选择规则、PNG 像素哈希与 16 项实际测试。",
    "goal": "明确采用 at、after 或零起帧索引，从真实解码帧中取图，再验收时间戳与 PNG 像素。任意输入秒数不一定对应已有的一帧。",
    "preparation": [
      "下载完整包并解压到自己控制的本地目录。配套视频是原创合成时间夹具，不是 H3 生成结果。",
      "准备 Python 3 与已有 FFmpeg。本次实跑环境为 macOS arm64、Python 3.12.3、FFmpeg 6.0，不需要 ffprobe 或额外 Python 包。",
      "FFmpeg 不在 PATH 时设置 FFMPEG_BIN。选择新的输出目录，夹具脚本与抽帧脚本都拒绝覆盖。",
      "处理自己的片段前，明确时间相对第一帧解码 PTS，还是别的时间原点；同时决定采用显示朝向或原始朝向。本脚本使用第一帧 PTS 相对秒数。"
    ],
    "prompt": "",
    "steps": [
      {
        "title": "01. 先建立解码帧时间表",
        "body": "先跑夹具，再处理正式片段。CFR 有 10 帧，时间为 0.00、0.10……0.90 秒；VFR 有 8 帧，为 0.00、0.04、0.12、0.28、0.32、0.60、0.64、0.92 秒。实测两者 time_base 都是 1/1000。CSV 与 inventory JSON 将每个零起解码索引对应到整数 PTS；秒数为 PTS 乘时间基准，不用平均 FPS 推算 VFR 位置。",
        "commands": [
          "export FFMPEG_BIN=/YOUR/INSTALLED/ffmpeg",
          "python3 run-fixtures.py --out-dir ./my-run"
        ]
      },
      {
        "title": "02. 明确 at、after 或精确解码索引",
        "body": "at 取目标之前或恰在目标上的最后一个帧起点，after 取之后或恰在目标上的第一个起点。VFR 目标 0.20 秒时，at 取索引 2 的 0.12 秒，after 取索引 3 的 0.28 秒。--rule frame --index 3 同样取 0.28 秒；精确索引不代表任意时间都能精确匹配。脚本从起点解码，时间超过首末 PTS 范围会拒绝，不猜测尾帧显示时长。",
        "commands": [
          "python3 extract-frame.py --input executed/vfr.mkv --rule at --time 0.20 --out ./at.png",
          "python3 extract-frame.py --input executed/vfr.mkv --rule after --time 0.20 --out ./after.png",
          "python3 extract-frame.py --input executed/vfr.mkv --rule frame --index 3 --out ./index3.png"
        ]
      },
      {
        "title": "03. 分开验收原始 PTS 与保存像素",
        "body": "查看 PNG 的 selection 回执与 inventory 附件，不只看文件名。源帧 framehash 保留时间戳，使用 passthrough 帧调度并明确时间基准；#tb 标头规定整数 PTS 的单位。脚本还会重新解码 PNG，确认 RGB 哈希与同朝向策略下选中的源帧一致。framehash 的数据包 duration 列不用于推测 VFR 显示停留；文件 SHA-256 核对字节，解码 RGB SHA-256 核对这次格式转换和朝向下的像素。",
        "commands": [
          "python3 -m json.tool at.selection.json",
          "python3 -m json.tool at.inventory.json"
        ]
      },
      {
        "title": "04. 选择旋转策略，保留失败检查",
        "body": "display 使用默认自动旋转，raw 在输入前应用 -noautorotate。实际 90 度元数据夹具得到 display 180×320、raw 320×180；还验证了起点为 2 秒的片段按相对时间取帧。这里没有覆盖所有显示矩阵、镜像或损坏时间戳。把朝向策略保留在回执里，遇到歧义停止，不通过强制 CFR 或插帧掩盖。",
        "commands": [
          "python3 extract-frame.py --input executed/rotation-90.mp4 --rule frame --index 0 --orientation display --out ./display.png",
          "python3 extract-frame.py --input executed/rotation-90.mp4 --rule frame --index 0 --orientation raw --out ./raw.png"
        ]
      }
    ],
    "acceptance": [
      "下载脚本实跑通过回执中的 16 个命名场景，并记录当次 Python、FFmpeg 版本与两份实际执行脚本的准确 SHA-256。",
      "CFR/VFR 表同时记录解码索引、整数 PTS 与有理数时间基准，包含每处 VFR 时间间隔。",
      "0.20 秒的 VFR at/after 输出分别对应表中的不同索引与 PTS，没有用合成的 0.20 秒帧代替。",
      "保存 PNG 的解码 RGB 哈希与同一朝向策略下的选中源帧一致。",
      "负时间、超过末帧 PTS 的时间和越界索引会报错停止。",
      "旋转尺寸符合明确的 display/raw 策略；非零起点采用第一帧 PTS 相对时间。",
      "回执没有把本机夹具通过扩展成模型画质、全部编码、全部显示变换或所有真实时间点都已验证。"
    ],
    "troubleshooting": [
      {
        "symptom": "取出的画面比输入秒数更早或更晚。",
        "change": "先看选择规则与真实 PTS 表。目标位于两个 VFR 时间戳之间时，at 与 after 本来就可能取不同帧，不把报告时间改成输入时间。"
      },
      {
        "symptom": "PNG 横竖方向不对，或宽高交换。",
        "change": "先明确希望的显示朝向，对比 display 与 raw。PNG 像素使用选定策略，不假定视频旋转 side data 在 PNG 中还会作为观看指令生效。"
      },
      {
        "symptom": "抽帧脚本拒绝视频末尾的一段时间。",
        "change": "本策略止于最后已知 PTS，不推测尾帧显示多久。可直接选择最后帧索引，或另外建立其显示时长后再扩展规则。"
      },
      {
        "symptom": "按 FPS 算出的索引与帧表不一致。",
        "change": "以解码呈现顺序与有理数时间基准为准。平均 FPS、数据包和关键帧属于不同编号体系，VFR 更不能混用。"
      },
      {
        "symptom": "另一套 FFmpeg 上哈希不同。",
        "change": "先在相同像素格式与朝向下比较解码 RGB，再比较压缩 PNG 的文件字节。记录新工具版本，配套回执只证明其中记录的运行。"
      }
    ],
    "ending": "把 PNG、选择回执与源帧时间表一起保存。明确规则、真实时间戳与像素检查让取帧可以复核，但不会制造源视频从未包含的时间点帧。",
    "assetCaption": "原创时间示意图与小型 RGB 合成夹具，附实际 PNG、framehash 日志和 16 场景回执。本配方是本机 FFmpeg 测试，不是模型生成结果。"
  },
  "assets": [],
  "artifacts": [
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/frame-extraction-kit.zip",
      "label": {
        "en": "Complete runnable fixture and extraction kit",
        "zh": "完整可运行抽帧与夹具包"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/extract-frame.py",
      "label": {
        "en": "Python frame selector with explicit timing rules",
        "zh": "明确时间规则的 Python 抽帧脚本"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/run-fixtures.py",
      "label": {
        "en": "Run 16 original media fixture checks",
        "zh": "运行 16 项原创媒体夹具检查"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/README.en.md",
      "label": {
        "en": "English commands, tables and verification",
        "zh": "英文命令、对应表与验收说明"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/README.zh.md",
      "label": {
        "en": "Chinese commands, tables and verification",
        "zh": "中文命令、对应表与验收说明"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/timestamp-table.csv",
      "label": {
        "en": "Actual CFR/VFR frame-index and PTS table",
        "zh": "实际 CFR/VFR 帧索引与 PTS 对应表"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/execution-receipt.json",
      "label": {
        "en": "Actual FFmpeg 6.0 execution receipt: 16 passed checks",
        "zh": "FFmpeg 6.0 实跑回执：16 项通过"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/executed/vfr-all.framehash.txt",
      "label": {
        "en": "All decoded VFR RGB frame hashes",
        "zh": "VFR 全部解码 RGB 帧哈希"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/executed/vfr-at-3.png",
      "label": {
        "en": "Verified at-selection PNG: frame 2 at 0.12 s",
        "zh": "已核对 at PNG：帧 2、0.12 秒"
      }
    },
    {
      "path": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/executed/vfr-after-4.png",
      "label": {
        "en": "Verified after-selection PNG: frame 3 at 0.28 s",
        "zh": "已核对 after PNG：帧 3、0.28 秒"
      }
    }
  ],
  "execution": {
    "pythonVersion": "3.12.3",
    "ffmpegVersion": "6.0",
    "command": "python3 run-fixtures.py --out-dir ./my-run",
    "receiptPath": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/execution-receipt.json",
    "passedScenarios": [
      "cfr-pts-inventory",
      "vfr-pts-inventory",
      "cfr-at-0.25",
      "cfr-after-0.25",
      "cfr-frame-3",
      "vfr-at-0.20",
      "vfr-after-0.20",
      "vfr-frame-3",
      "vfr-at-0.28",
      "vfr-after-0.28",
      "negative-time",
      "beyond-last-pts",
      "frame-out-of-range",
      "rotation-display",
      "rotation-raw",
      "nonzero-start-relative-time"
    ],
    "productionTaskRun": false,
    "fixtureOnly": true,
    "scriptHashes": {
      "extract-frame.py": "5aaa82ad8834c8542af3c894e9e979db9d1321886bd15c80a236b7037b87979a",
      "run-fixtures.py": "c26ce34b48c8b18af865d46347671e8a05ad1ca319c9f74a554170285029c756"
    },
    "scriptBinding": "SHA-256 captured before execution and rechecked afterward"
  },
  "hero": {
    "imagePath": "/learning-assets/pseo-batch2-20261014/frame-extraction-timebase-validation/selection-timeline.png",
    "label": {
      "en": "Original VFR frame-selection timing diagram",
      "zh": "原创 VFR 取帧时间对应示意"
    }
  },
  "sources": [
    {
      "title": "FFmpeg command-line documentation: fps_mode, enc_time_base, copyts and autorotate",
      "url": "https://ffmpeg.org/ffmpeg.html"
    },
    {
      "title": "FFmpeg filters: select, showinfo and settb",
      "url": "https://ffmpeg.org/ffmpeg-filters.html#select_002c-aselect"
    },
    {
      "title": "FFmpeg framehash muxer documentation",
      "url": "https://ffmpeg.org/ffmpeg-formats.html#framehash"
    }
  ],
  "shots": [],
  "demonstration": null
}
