#!/usr/bin/env python3
"""reframe.py — auto-reframe: find where the speaker's face sits across a clip and
return the ffmpeg crop (for a target aspect) centered on them. Replaces hand-picking
the crop x-offset. Lean face-track (OpenCV Haar) — the mechanism ClipsAI's resize
uses (facenet), minus the torch/pyannote multi-speaker stack. Single-speaker friendly.

Runs in tools/reframe-venv (python 3.11 + opencv). Prints JSON:
    {"crop": "cropW:cropH:x:y", "faces": N, "cx": .., "cy": .., "source": "WxH"}
`crop` is a drop-in for ffmpeg `crop=<crop>,scale=...`. Falls back to a centered crop
(faces:0) so a render never breaks.
"""
import argparse
import json
import sys

import cv2


def _even(n):
    n = int(round(n))
    return n - (n % 2)


def reframe(video, start, end, aspect=(9, 16), samples=24):
    cap = cv2.VideoCapture(video)
    if not cap.isOpened():
        raise SystemExit(json.dumps({"error": "cannot open video"}))
    W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    dur = max(0.1, float(end) - float(start))
    casc = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")

    centers = []  # (cx, cy, area) of the largest face per sampled frame
    for i in range(samples):
        t = float(start) + dur * (i + 0.5) / samples
        cap.set(cv2.CAP_PROP_POS_MSEC, t * 1000.0)
        ok, frame = cap.read()
        if not ok:
            continue
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = casc.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=6,
                                      minSize=(max(40, W // 18), max(40, H // 18)))
        if len(faces):
            x, y, w, h = max(faces, key=lambda f: f[2] * f[3])   # biggest face
            centers.append((x + w / 2, y + h / 2, w * h))
    cap.release()

    # target crop rect (largest rect of the aspect that fits the source)
    r = aspect[0] / aspect[1]
    cropW, cropH = (_even(H * r), H) if H * r <= W else (W, _even(W / r))

    if centers:
        centers.sort(key=lambda c: c[0])                         # median-x is robust to a stray detection
        cx = centers[len(centers) // 2][0]
        cy = sum(c[1] for c in centers) / len(centers)
    else:
        cx, cy = W / 2, H / 2                                     # no face → center (never breaks)

    x = _even(min(max(cx - cropW / 2, 0), W - cropW))
    y = _even(min(max(cy - cropH / 2, 0), H - cropH))
    return {"crop": f"{cropW}:{cropH}:{x}:{y}", "faces": len(centers),
            "cx": round(cx), "cy": round(cy), "source": f"{W}x{H}"}


if __name__ == "__main__":
    a = argparse.ArgumentParser()
    a.add_argument("--video", required=True)
    a.add_argument("--start", type=float, default=0)
    a.add_argument("--end", type=float, default=30)
    a.add_argument("--aspect", default="9:16")
    a.add_argument("--samples", type=int, default=24)
    p = a.parse_args()
    asp = tuple(int(x) for x in p.aspect.split(":"))
    print(json.dumps(reframe(p.video, p.start, p.end, asp, p.samples)))
