"""3인 와이드샷에서 **누구의 입이 언제 움직였나**를 프레임 차분으로 관측한다.

컷이 없는 8초 단일 샷이라 인물 위치가 고정이다 — 그래서 얼굴 박스를 상수로 두고
입 영역(얼굴 박스 하단 1/3)의 프레임간 차분 에너지만 시계열로 뽑는다.
포즈·랜드마크 추정이 아니라 "그 영역이 움직였나" 까지만 보는 대리지표다.

★박스가 어긋나면 값이 통째로 흔들린다 — 그래서 박스를 ±8px 밀어본 5판의 범위를 함께 낸다
  (feasibility ⑤ 에서 박스 노이즈로 판정이 뒤집힌 선례가 있다).
"""
import json
import subprocess
import sys
from pathlib import Path

import numpy as np

W, H, FPS = 768, 1280, 24
# (name, x0, y0, x1, y1) — 얼굴 박스. 입은 이 박스 하단 1/3 로 잡는다.
# ★박스는 **클립마다 다르다** — 같은 seed·같은 레퍼런스라도 프레이밍이 달라진다(2단은 와이드,
#   1단 대조군은 더 붙은 샷). 2단 박스를 1단에 그대로 쓰면 엉뚱한 영역을 재고도 숫자는 나온다.
FACES_BY_CLIP = {
    "out_stage2_j26680.mp4": [
        ("P1_hanna_left", 100, 667, 167, 773),
        ("P2_yumin_mid", 353, 687, 420, 780),
        ("P3_jiho_right", 600, 573, 673, 680),
    ],
    # 단일화자 패스스루 (t_1314f9823626 · 158f) — 3인 와이드, 컷 없음. 지호는 2단보다 카메라에
    # 가까워 얼굴이 크고 위쪽에 있다(박스를 2단에서 가져오면 안경을 입으로 잰다 — 실제로 처음에 그랬다).
    "h3_r2v_passthru-yumin-passthru_00001_.mp4": [
        ("P1_hanna_left", 185, 520, 265, 645),
        ("P2_yumin_mid", 335, 540, 420, 665),
        ("P3_jiho_right", 605, 340, 700, 505),
    ],
    "out_stage1ctl_j26681.mp4": [
        ("P1_hanna_left", 127, 560, 233, 693),
        ("P2_yumin_mid", 353, 567, 467, 707),
        ("P3_jiho_right", 567, 400, 693, 560),
    ],
}


def frames(path):
    raw = subprocess.run(
        ["ffmpeg", "-v", "error", "-i", path, "-vf", f"scale={W}:{H},format=gray",
         "-f", "rawvideo", "-"], capture_output=True, check=True).stdout
    return np.frombuffer(raw, dtype=np.uint8).reshape(-1, H, W).astype(np.float32)


def mouth_series(fr, box, dx=0, dy=0):
    _, x0, y0, x1, y1 = box
    my0 = y0 + int((y1 - y0) * 2 / 3)                     # 하단 1/3 = 입
    r = fr[:, my0 + dy:y1 + dy, x0 + dx:x1 + dx]
    return np.abs(np.diff(r, axis=0)).mean(axis=(1, 2))


def main():
    path = sys.argv[1]
    faces = FACES_BY_CLIP[Path(path).name]          # 등록 안 된 클립은 큰소리로 죽는다
    fr = frames(path)
    out = {"video": path, "n_frames": len(fr), "fps": FPS, "per_face": {}}
    for box in faces:
        base = mouth_series(fr, box)
        shifts = [mouth_series(fr, box, dx, dy)
                  for dx, dy in [(0, 0), (8, 0), (-8, 0), (0, 8), (0, -8)]]
        # 0.5초 창으로 묶어 읽기 쉽게 (프레임 단위는 노이즈가 크다)
        step = FPS // 2
        binned = [round(float(base[i:i + step].mean()), 3) for i in range(0, len(base), step)]
        band = [[round(float(np.min([s[i:i + step].mean() for s in shifts])), 3),
                 round(float(np.max([s[i:i + step].mean() for s in shifts])), 3)]
                for i in range(0, len(base), step)]
        out["per_face"][box[0]] = {"bin_0.5s": binned, "box_shift_band": band}
    print(json.dumps(out, ensure_ascii=False, indent=1))


if __name__ == "__main__":
    main()
