"""H3 ④ 캐릭터 교체 — 원본 driving 대비 산출물의 추종 강도·오디오를 관측값으로 뽑는다.

두 축만 잰다:
  ⓐ 모션 타이밍 — 프레임간 변화량(motion energy) 시계열의 상호상관 → lag(프레임)와 상관계수.
     포즈 추정이 아니라 "언제 움직였나" 만 보는 대리지표다. lag=0 · 상관 높음 = 프레임 정합 방향.
  ⓑ 오디오 — RMS 포락선·무음 비율·스펙트럼 중심. 음성/음악 판별은 사람 몫이고, 여기선
     "소리가 났나 · 언제 났나 · 대역이 어디인가" 까지만 관측한다.
"""
import subprocess, sys, json
import numpy as np

W, H = 64, 112  # 다운스케일 그레이스케일 (9:16 근사)


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


def motion_energy(frames):
    d = np.abs(np.diff(frames, axis=0)).mean(axis=(1, 2))
    return (d - d.mean()) / (d.std() + 1e-9)


def xcorr_lag(a, b, max_lag=24):
    """a 를 기준으로 b 를 lag 만큼 밀었을 때의 최대 상관과 그 lag."""
    best = (-2.0, 0)
    for lag in range(-max_lag, max_lag + 1):
        if lag >= 0:
            x, y = a[lag:], b[:len(b) - lag] if lag else b
        else:
            x, y = a[:len(a) + lag], b[-lag:]
        n = min(len(x), len(y))
        if n < 16:
            continue
        c = float(np.corrcoef(x[:n], y[:n])[0, 1])
        if c > best[0]:
            best = (c, lag)
    return best


def audio_stats(path):
    cmd = ["ffmpeg", "-v", "error", "-i", path, "-ac", "1", "-ar", "16000",
           "-f", "f32le", "-"]
    p = subprocess.run(cmd, capture_output=True)
    raw = p.stdout
    if not raw:
        return {"no_audio_stream": True, "ffmpeg_rc": p.returncode}
    x = np.frombuffer(raw, dtype=np.float32)
    sr, hop = 16000, 400  # 25ms
    n = len(x) // hop
    fr = x[:n * hop].reshape(n, hop)
    rms = np.sqrt((fr ** 2).mean(axis=1))
    db = 20 * np.log10(rms + 1e-9)
    # 스펙트럼 중심 (전체 구간 평균)
    spec = np.abs(np.fft.rfft(fr * np.hanning(hop), axis=1))
    freqs = np.fft.rfftfreq(hop, 1 / sr)
    centroid = float((spec * freqs).sum() / (spec.sum() + 1e-9))
    return {
        "duration_s": round(len(x) / sr, 3),
        "peak_dbfs": round(float(db.max()), 1),
        "median_dbfs": round(float(np.median(db)), 1),
        "silent_frac_below_-45dB": round(float((db < -45).mean()), 3),
        "spectral_centroid_hz": round(centroid, 1),
        "rms_db_per_0.5s": [round(float(v), 1) for v in
                            db[:n // 20 * 20].reshape(-1, 20).max(axis=1)] if n >= 20 else [],
    }


if __name__ == "__main__":
    driving, out = sys.argv[1], sys.argv[2]
    df, of = gray_frames(driving), gray_frames(out)
    dm, om = motion_energy(df), motion_energy(of)
    n = min(len(dm), len(om))
    corr, lag = xcorr_lag(dm[:n], om[:n])
    res = {
        "driving": {"path": driving, "frames": int(len(df))},
        "output": {"path": out, "frames": int(len(of))},
        "motion_xcorr": {"best_corr": round(corr, 3), "best_lag_frames": lag,
                         "corr_at_lag0": round(float(np.corrcoef(dm[:n], om[:n])[0, 1]), 3)},
        "audio_driving": audio_stats(driving),
        "audio_output": audio_stats(out),
    }
    print(json.dumps(res, ensure_ascii=False, indent=2))
