#!/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)
