#!/usr/bin/env python3 """Standalone DE-optimised AUDIO scene cutter: tune a matched-filter ramp on the audio log-PSD to maximise X-Ray boundary F1. No neural net. Holdout films are never seen in training. Writes the tuned filter + held-out performance.""" import sys, json, os import numpy as np sys.path.insert(0, "scripts/scene_detector") from de_ramp import load_series, xray_bounds, ramp_kernel, response, boundary_f1 from scipy.optimize import differential_evolution MANIFEST = "experiments/manifests/films_LVFace_opencv5.json" AUDIO = "experiments/dumps/audio_features" HOLDOUT = {"Scarface", "Sound_of_Metal", "Valerian_and_the_City_of_a_Thousand_Plan"} OUT = "experiments/results/scene_boundary/de_audio_cutter.json" films = json.load(open(MANIFEST)) train = [f for f in films if f["slug"] not in HOLDOUT] val = [f for f in films if f["slug"] in HOLDOUT] tr = [(load_series(f["dump"], AUDIO, "audio")[0], xray_bounds(f["xray"])) for f in train] va = [(f["slug"], load_series(f["dump"], AUDIO, "audio")[0], xray_bounds(f["xray"])) for f in val] print(f"DE AUDIO cutter: {len(tr)} train, holdout {sorted(HOLDOUT)}", flush=True) def neg_f1(x): H = int(round(x[0])); gamma = x[1]; dead = int(round(x[2])); pct = x[3] if H < 1 or dead >= H: return 0.0 w = ramp_kernel(H, gamma, dead) return -float(np.mean([boundary_f1(response(s, w, H), b, pct) for s, b in tr])) evals = [0] def cb(xk, convergence): evals[0] += 1 print(f"[de-audio] gen {evals[0]} convergence={convergence:.3f}", flush=True) res = differential_evolution(neg_f1, [(1, 10), (0.3, 3.0), (0, 4), (80, 98)], seed=0, popsize=12, maxiter=25, tol=1e-4, polish=False, callback=cb) H = int(round(res.x[0])); gamma = float(res.x[1]); dead = int(round(res.x[2])); pct = float(res.x[3]) print(f"\n=== DE-OPTIMISED AUDIO SCENE CUTTER ===", flush=True) print(f"tuned ramp: H={H}s gamma={gamma:.2f} dead={dead}s threshold_pct={pct:.0f}", flush=True) print(f"train boundary-F1: {-res.fun*100:.1f}%\n", flush=True) print("held-out (audio-only, P/R/F1 ±2s):", flush=True) w = ramp_kernel(H, gamma, dead) rep = {"H": H, "gamma": gamma, "dead": dead, "pct": pct, "train_f1": float(-res.fun), "holdout": sorted(HOLDOUT), "films": {}} for slug, s, b in va: r = response(s, w, H); thr = np.percentile(r, pct); pred = np.where(r > thr)[0] bidx = [int(x) for x in b if int(x) < len(r)] tp_p = sum(any(abs(p-i) <= 2 for i in bidx) for p in pred) tp_t = sum(any(abs(p-i) <= 2 for p in pred) for i in bidx) P = tp_p/max(len(pred), 1); R = tp_t/max(len(bidx), 1); F = 2*P*R/(P+R) if P+R else 0 rep["films"][slug] = {"P": P, "R": R, "F1": F, "n_pred": len(pred), "n_true": len(bidx)} print(f" {slug[:26]:26s} P={P*100:4.0f}% R={R*100:4.0f}% F1={F*100:4.0f}% " f"({len(pred)} preds/{len(bidx)} true)", flush=True) os.makedirs(os.path.dirname(OUT), exist_ok=True) json.dump(rep, open(OUT, "w"), indent=2) print(f"\nsaved → {OUT}", flush=True)