Fix round, item 9. The streaming segmenter kept every frame's score, so its memory grew with the stream. Scores are now an opt-in batch output (Segmenter::with_scores, which segment() uses); a streaming segmenter holds at most 2L + 2 frame records and the window, and Segmenter::retained says how much. A test pushes 60,000 frames and checks the bound at every one. segment_diff.py (from the review's segdiff) compares the streaming segmenter, through the wai-pack segment-json aid, with pack_verify.py's on-demand form on seeded random clips, and checks the min and max bounds of every piece; CI runs it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""wai.pack.scenecut/1, differentially: wai-pack's streaming segmenter against
|
|
pack_verify.py's on-demand form.
|
|
|
|
Seeded random clips (flat and noisy shots, cuts, flashes, small perturbations, empty and
|
|
one-frame clips) under random parameters; every piece (start, length, flags and the
|
|
statistics classification reads) and every score from `wai-pack segment-json`, pushed
|
|
a frame at a time or in a batch, must equal pack_verify.py's, and every piece but the
|
|
last must lie between `min` and `max` frames (the last at most `max`).
|
|
|
|
python3 segment_diff.py <wai-pack binary> [seed] [count]
|
|
|
|
Exit status 0 only if every clip agrees.
|
|
"""
|
|
import json
|
|
import os
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HERE)
|
|
import pack_verify as PV # noqa: E402
|
|
|
|
|
|
def clip(rng):
|
|
w, h = rng.randint(1, 5), rng.randint(1, 4)
|
|
scenes = []
|
|
for _ in range(rng.randint(1, 5)):
|
|
if rng.random() < 0.5:
|
|
scenes.append([rng.randrange(256)] * (w * h * 3))
|
|
else:
|
|
scenes.append([rng.randrange(256) for _ in range(w * h * 3)])
|
|
n = rng.randint(0, 40) if rng.random() < 0.9 else rng.randint(0, 3)
|
|
frames, cur = [], 0
|
|
for _ in range(n):
|
|
if rng.random() < 0.15:
|
|
cur = rng.randrange(len(scenes))
|
|
f = scenes[cur]
|
|
if rng.random() < 0.1:
|
|
f = scenes[rng.randrange(len(scenes))]
|
|
if rng.random() < 0.2:
|
|
f = [min(255, max(0, x + rng.randint(-6, 6))) for x in f]
|
|
frames.append(list(f))
|
|
mn = rng.randint(1, 6)
|
|
tg = mn + rng.randint(0, 6)
|
|
mx = tg + rng.randint(0, 8)
|
|
p = {"min": mn, "target": tg, "max": mx, "cut_abs": rng.choice([1, 50, 100, 300, 500, 1000]),
|
|
"cut_rel": rng.choice([0, 1000, 3000, 1000000]), "window": rng.randint(1, 8), "lookahead": rng.randint(0, 7)}
|
|
return {"w": w, "h": h, "p": p, "frames": frames, "stream": rng.random() < 0.5}
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) < 2:
|
|
print(__doc__)
|
|
return 2
|
|
binary = argv[1]
|
|
seed = int(argv[2]) if len(argv) > 2 else 1
|
|
count = int(argv[3]) if len(argv) > 3 else 2000
|
|
rng = random.Random(seed)
|
|
cases = [clip(rng) for _ in range(count)]
|
|
r = subprocess.run([binary, "segment-json"], input=("\n".join(json.dumps(c) for c in cases) + "\n").encode(),
|
|
capture_output=True)
|
|
outs = r.stdout.decode().splitlines()
|
|
if r.returncode != 0 or len(outs) != count:
|
|
print(r.stderr.decode(errors="replace")[-2000:])
|
|
return 1
|
|
bad = 0
|
|
for c, o in zip(cases, outs):
|
|
ro = json.loads(o)
|
|
if not c["frames"]:
|
|
ok = ro["pieces"] == []
|
|
else:
|
|
pp, ps = PV.segment(c["frames"], c["w"], c["h"], c["p"])
|
|
py = [[x["start"], x["len"], x["flags"], x["abs_diff"], x["changed"], x["compared"], x["occupancy"]] for x in pp]
|
|
ok = py == ro["pieces"] and ps == ro["scores"]
|
|
pcs = ro["pieces"]
|
|
for i, x in enumerate(pcs):
|
|
if x[1] > c["p"]["max"] or (i + 1 < len(pcs) and x[1] < c["p"]["min"]):
|
|
ok = False
|
|
if not ok:
|
|
bad += 1
|
|
if bad <= 5:
|
|
print("DIFF", json.dumps(c["p"]), "frames", len(c["frames"]), "stream", c["stream"])
|
|
print(f"segment_diff: seed {seed}, {count} clips, {bad} differences")
|
|
return 0 if bad == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|