Files
ECC/skills/taste-application/scripts/verify.py
T
928c1dea72 feat(tasteforge): package reusable workflows and preserve native edits (#3033)
* feat: bundle standalone taste distillation and application workflows

* docs: fix imported taste skill markdown lint

* docs: align Turkish agent catalog with taste skills

* refactor: make ECC the canonical reusable video engine

* fix: preserve video duration when applying image overlays

* fix: preserve background colors in image compositing

* fix: report best-effort duration targets and shortfalls

* feat: ship verified Fusion presets with compatibility provenance

* feat(tasteforge): preserve native edits in application bundles

* feat(tasteforge): compile local preservation without hosted input

* fix: update js-yaml to patched 4.3.2

* test: report bounded Stop wrapper failure diagnostics

* fix(tasteforge): fail closed on unsafe output names, missing overlays and cadence

- cli: default report and spec paths are derived from pack name and profile
  genre; require the manifest's name pattern before using either as a
  filename part so a traversal string cannot write outside cwd/out.
- apply_local: a pack without cadence.json, or with no measured shots and
  no explicit mean_shot, raises instead of silently planning 1.0s shots and
  reporting a measured cadence.
- legacy apply: a missing overlay aborts before any paid upload; forge()
  would have rejected it after every take was generated.
- requirements-live: pin fal-client>=0.13.0, the first release whose
  subscribe() accepts client_timeout.

Addresses the five P1 findings from the independent review of #3033.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fxHRsydPqEcYngGbqkgt1

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 15:31:36 +01:00

229 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""Measure a finished video against the pack it was supposed to match.
Every other stage of this pipeline claims a result. This one checks it, and it
exists because of a specific failure: a graded clip once scored a chroma mean
absolute error of 1.88 and a contrast of 33.7 against a 34.7 target - both
excellent - while the actual frame was a muddy purple mess with visible
banding. The numbers were real and the picture was wrong.
The cause was that CDF tone matching forced a generated clip whose frame was
68% pure black onto a reference histogram that was not, which lifted the entire
background out of black and spread quantisation error across it. No chroma or
contrast statistic can see that, because both are computed over all pixels and
the background is still, on average, dark.
So this suite checks distribution *shape*, not just distribution *moments*:
* ``background`` - share of the frame below L*10, source vs output vs target.
A source that was 68% black and an output that is 26% black is a broken
grade regardless of what the other numbers say.
* ``chroma`` - per-zone a*/b* error, which is what the grade is actually for.
* ``tone`` - contrast, black and white points.
* ``cadence`` - detected cut rhythm against the reference's.
* ``banding`` - count of L* histogram bins that are empty between occupied
neighbours; comb-like gaps are the signature of a stretched tone curve.
Exit status is non-zero if any check fails, so it can gate a pipeline run.
python verify.py --genre flashethereal out/FINAL.mp4 --source gen/run3.mp4
"""
from __future__ import annotations
import argparse
import json
import math
import sys
from pathlib import Path
import cv2
import numpy as np
from taste import cadence as cad_mod
from taste import frames as frame_mod
from taste import grade as grade_mod
from taste import pack as pack_mod
SHADOW_L = 10.0 # L* below this reads as "black background" on screen
def _lab(path: str | Path, n: int = 40) -> np.ndarray:
"""Pooled Lab pixels. Float32 input, so L* is 0-100 and a*/b* are signed.
Worth stating explicitly because OpenCV changes convention with dtype:
on uint8 input it packs L into 0-255 and biases a*/b* by +128, and mixing
the two conventions silently reports chroma errors in the hundreds.
"""
fr = frame_mod.sample_frames(path, n=n)
pix = np.concatenate([f.reshape(-1, 3) for f in fr], axis=0).astype(np.float32)
return cv2.cvtColor(pix.reshape(-1, 1, 3), cv2.COLOR_RGB2LAB).reshape(-1, 3)
def background_share(lab: np.ndarray, thresh: float = SHADOW_L) -> float:
"""Share of pixels dark enough to read as unlit background."""
return float((lab[:, 0] < thresh).mean())
def banding_score(lab: np.ndarray, bins: int = 256) -> int:
"""Empty L* histogram bins that sit between two occupied ones.
A tone curve that stretches a narrow input range leaves periodic gaps -
the comb pattern you see on a scope right before banding shows up on the
picture. Counting interior holes catches it; counting total empty bins
does not, because a legitimately dark clip has empty highlight bins.
"""
h, _ = np.histogram(lab[:, 0], bins=bins, range=(0, 100))
occ = h > 0
idx = np.flatnonzero(occ)
if len(idx) < 3:
return 0
return int((~occ[idx[0]:idx[-1] + 1]).sum())
def zone_chroma(lab: np.ndarray) -> list[tuple[float, float]]:
out = []
for lo, hi in zip(grade_mod.ZONE_EDGES[:-1], grade_mod.ZONE_EDGES[1:]):
m = (lab[:, 0] >= lo) & (lab[:, 0] < hi)
if m.sum() < 64:
out.append((0.0, 0.0))
continue
sel = lab[m]
# Median, matching how the pack's own zone targets were measured;
# a mean here would compare a skew-sensitive statistic against a
# robust one and report an error that is really a definition mismatch.
out.append((float(np.median(sel[:, 1])), float(np.median(sel[:, 2]))))
return out
def verify(
video: str,
genre: str,
root: str = "stylepacks",
source: str | None = None,
bg_tolerance: float = 0.20,
chroma_tolerance: float = 6.0,
contrast_tolerance: float = 5.0,
cadence_tolerance: float = 0.35,
check_cadence: bool = True,
) -> dict:
sp = pack_mod.load(genre, root=root)
tgt = grade_mod.load_stats(sp.grade_path)
ref_cad = cad_mod.load(sp.cadence_path)
out_lab = _lab(video)
src_lab = _lab(source) if source and Path(source).exists() else None
checks: list[dict] = []
def check(name: str, ok: bool, got, want, note: str = "") -> None:
checks.append({"check": name, "pass": bool(ok), "got": got, "want": want,
"note": note})
# ---- tone ----------------------------------------------------------
L = out_lab[:, 0]
black = float(np.percentile(L, 1))
white = float(np.percentile(L, 99))
# Contrast is the standard deviation of L*, which is what GradeStats
# records - NOT the white-minus-black range. The range is nearly always
# ~100 on real footage and so discriminates nothing.
contrast = float(L.std())
check("contrast", abs(contrast - tgt.contrast) <= contrast_tolerance,
round(contrast, 2), round(tgt.contrast, 2))
check("black_point", black <= tgt.black_point + 3.0,
round(black, 2), f"<= {tgt.black_point + 3.0:.1f}")
check("white_point", abs(white - tgt.white_point) <= 8.0,
round(white, 2), round(tgt.white_point, 2))
# ---- chroma by zone --------------------------------------------------
got_zones = zone_chroma(out_lab)
errs = []
for (ga, gb), z in zip(got_zones, tgt.zones):
errs.append(abs(ga - z[0]) + abs(gb - z[2]))
mae = float(np.mean(errs) / 2.0) if errs else 0.0
check("chroma_mae", mae <= chroma_tolerance, round(mae, 2),
f"<= {chroma_tolerance}")
# ---- background preservation ----------------------------------------
# The comparison is against the REFERENCE, not against the source clip.
# Anchoring on the source is the tempting version and it is wrong in both
# directions: this pack's references are 24-55% black while one generated
# source came in at 68%, so "preserve the source's blacks" would demand an
# output blacker than anything the reference ever was, and would equally
# excuse a grade that lifted an already-crushed source. What matters is
# landing where the reference lives.
bg_out = background_share(out_lab)
# GradeStats defaults absent legacy fields to zero. Inspect the stored
# field so a measured zero remains a real target rather than a missing one.
bg_value = json.loads(Path(sp.grade_path).read_text(encoding="utf-8")).get("bg_share")
bg_valid = (isinstance(bg_value, (int, float)) and not isinstance(bg_value, bool)
and math.isfinite(bg_value) and 0 <= bg_value <= 1)
if bg_valid:
bg_tgt = float(bg_value)
drift = abs(bg_out - bg_tgt)
note = "share of frame reading as unlit background"
if src_lab is not None:
bg_src = background_share(src_lab)
note += f"; source was {100 * bg_src:.1f}%"
check("background", drift <= bg_tolerance,
f"{100 * bg_out:.1f}%", f"{100 * bg_tgt:.1f}% +/- {100 * bg_tolerance:.0f}",
note)
elif bg_value is not None:
check("background", False, f"{100 * bg_out:.1f}%", "finite bg_share in [0, 1]",
"pack contains an invalid bg_share; re-run mint.py")
else:
checks.append({"check": "background", "pass": None,
"got": f"{100 * bg_out:.1f}%", "want": "n/a",
"note": "pack predates bg_share; re-run mint.py"})
# ---- banding ---------------------------------------------------------
holes = banding_score(out_lab)
src_holes = banding_score(src_lab) if src_lab is not None else 0
check("banding", holes <= max(8, src_holes + 8), holes,
f"<= {max(8, src_holes + 8)}",
"interior gaps in the L* histogram")
# ---- cadence ---------------------------------------------------------
if check_cadence:
got_cad = cad_mod.detect(video)
rel = abs(got_cad.mean_shot - ref_cad.mean_shot) / max(1e-6, ref_cad.mean_shot)
check("cadence", rel <= cadence_tolerance,
f"{got_cad.mean_shot:.2f}s / {got_cad.cuts_per_min:.0f} cpm",
f"{ref_cad.mean_shot:.2f}s / {ref_cad.cuts_per_min:.0f} cpm",
f"{100 * rel:.0f}% off")
passed = [c for c in checks if c["pass"] is True]
failed = [c for c in checks if c["pass"] is False]
print(f"\n === verify {Path(video).name} against '{genre}' ===")
for c in checks:
mark = "ok " if c["pass"] else ("SKIP" if c["pass"] is None else "FAIL")
note = f" ({c['note']})" if c["note"] else ""
print(f" [{mark}] {c['check']:22s} got {c['got']} want {c['want']}{note}")
print(f"\n {len(passed)} passed, {len(failed)} failed, "
f"{len(checks) - len(passed) - len(failed)} skipped")
return {"video": str(video), "genre": genre, "checks": checks,
"passed": len(passed), "failed": len(failed)}
def main() -> None:
ap = argparse.ArgumentParser(description="Verify a finished video against its style pack.")
ap.add_argument("video")
ap.add_argument("--genre", required=True)
ap.add_argument("--root", default="stylepacks")
ap.add_argument("--source", default=None,
help="the ungraded clip; adds background context and a banding baseline")
ap.add_argument("--no-cadence", action="store_true", help="skip shot detection (slow)")
ap.add_argument("--json", dest="json_out", default=None)
a = ap.parse_args()
res = verify(a.video, a.genre, a.root, a.source, check_cadence=not a.no_cadence)
if a.json_out:
Path(a.json_out).write_text(json.dumps(res, indent=2), encoding="utf-8")
sys.exit(1 if res["failed"] else 0)
if __name__ == "__main__":
main()