#!/usr/bin/env python3
"""Compose AI4M 'Mi IA dijo que mis proyectos son SUYOS' — GUION COMPLETO (~2:24, 5 segmentos):
AV1(hook+contexto+voz) + voz_waveform(audio real) + AV2(pregunta) + hero_waveform(giro audio real) + AV3(angulos+POSICION+CTA).
Captions del SPEC (serif dinamico + mix + safe-zone) en los avatares, push-in en el hook, b-rolls reales.
AUDIO: 2-pass loudnorm a -14 LUFS POR SEGMENTO antes de concat (lección ai4m-ia-mia: NUNCA 1-pass)."""
import subprocess, pathlib, sys, re, json
sys.path.insert(0, "/home/clawd/bin/agentsquad-shorts")
from lib import captions

BASE = "/home/clawd/playgrounds/ai4m-ia-mia"
WORK = pathlib.Path("/tmp/ia-mia-full"); WORK.mkdir(exist_ok=True)
OUTF = f"{BASE}/final_full.mp4"
CAP_Y = 1075
CHARF = 0.362; SERIF_TARGET_W = 760; SERIF_MAXFS = 236; SERIF_RATIO = 0.72
STYLES = {"Box": (94, 17, 132), "Serif": (200, 10, 144)}

def _ts(t):
    h=int(t//3600); m=int((t%3600)//60); s=t%60; return f"{h:d}:{m:02d}:{s:05.2f}"
def _wrap(text, maxc, cap=3):
    words=text.split(); lines=[]; cur=""
    for w in words:
        if cur and len(cur)+1+len(w)>maxc: lines.append(cur); cur=w
        else: cur=(cur+" "+w).strip()
    if cur: lines.append(cur)
    return lines[:cap]
def _wrap_serif(text):
    words=text.split()
    if not words: return [text]
    mw=max(len(w) for w in words)
    for cand in range(mw, len(text)+2):
        l=_wrap(text, cand, cap=99)
        if len(l)<=3: return l
    return _wrap(text, len(text)+2, cap=99)[:3]
def fix_brand(ws):
    out=[]; i=0
    while i < len(ws):
        w=ws[i]; wl=str(w.get("word","")).strip().lower().strip(".,!?")
        if wl=="ai" and i+2<len(ws):
            w1=str(ws[i+1].get("word","")).strip().lower().strip(".,!?")
            w2raw=str(ws[i+2].get("word","")).strip(); w2=w2raw.lower().strip(".,!?")
            if w1 in ("for","form","four","fore") and w2.startswith("manager"):
                trail="." if w2raw.endswith(".") else ""
                out.append({"word":"AI4Managers"+trail,"start":float(w.get("start",0)),"end":float(ws[i+2].get("end",0))}); i+=3; continue
        out.append(w); i+=1
    return out

def build_ass(words, out_path, sup, hook_end=0.0):
    head=("[Script Info]\nScriptType: v4.00+\nPlayResX: 1080\nPlayResY: 1920\nScaledBorderAndShadow: yes\nWrapStyle: 2\n\n"
          "[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
          "Style: Box,DejaVu Sans,94,&H00141414,&H00141414,&H00FFFFFF,&H00FFFFFF,1,0,0,0,100,100,1,0,3,9,0,5,60,60,0,1\n"
          "Style: Serif,PP Editorial New,200,&H00FFFFFF,&H00FFFFFF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,0,2,5,60,60,0,1\n\n"
          "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
    def sub_sup(t0,t1):
        segs=[(t0,t1)]
        for ws,we in sup:
            out=[]
            for a,b in segs:
                if we<=a or ws>=b: out.append((a,b)); continue
                if a<ws: out.append((a,ws))
                if we<b: out.append((we,b))
            segs=out
        return [(a,b) for a,b in segs if b-a>0.30]
    chunks=captions._phrase_chunks(words)
    starts=[float(c[0]["start"]) for c in chunks]
    ev=[]
    for i,ch in enumerate(chunks):
        txt=" ".join(str(w["word"]).strip() for w in ch).strip(); txt=re.sub(r"\s+"," ",txt)
        if not txt: continue
        t0=float(ch[0]["start"]); t1=float(ch[-1].get("end",t0+0.4))+0.12
        if i+1<len(starts): t1=min(t1, starts[i+1]-0.02)
        if t1<=t0: t1=t0+0.20
        style="Box" if (i%2==0 or txt.rstrip().endswith("?")) else "Serif"
        if t0<hook_end: style="Serif"
        fs,maxc,lh=STYLES[style]
        if style=="Serif":
            wl=_wrap_serif(txt); longest=max(len(l) for l in wl) if wl else 1
            fs=min(SERIF_MAXFS, int(SERIF_TARGET_W/(longest*CHARF))); lh=int(round(SERIF_RATIO*fs))
        else:
            wl=_wrap(txt, maxc)
        N=len(wl); segs=sub_sup(t0,t1)
        if not segs: continue
        for k,line in enumerate(wl):
            y=int(CAP_Y+(k-(N-1)/2.0)*lh)
            eff=("\\fs%d\\pos(540,%d)"%(fs,y)) if style=="Serif" else ("\\pos(540,%d)"%y)
            for a,b in segs:
                ev.append(f"Dialogue: 0,{_ts(a)},{_ts(b)},{style},,0,0,0,,{{{eff}}}{line}")
    open(out_path,"w").write(head+"\n".join(ev)+"\n")

def run(cmd):
    r=subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode!=0: raise RuntimeError("FAIL: "+" ".join(map(str,cmd))+"\n"+r.stderr[-1800:])

def find_window(words, anchors, before, after):
    """ventana [t-before, t+after] alrededor de la 1a palabra-ancla encontrada."""
    for x in words:
        wd=str(x.get("word","")).strip().lower().strip(".,?¿!")
        if wd in anchors:
            t=float(x.get("start",0)); return (max(0.0,t-before), t+after)
    return None

def process(idx, av, script, broll_specs, hook=False):
    """av captions + push-in(hook) + b-rolls (por ancla) -> seg{idx}.mp4 con audio CRUDO (sin loudnorm)."""
    w=captions.transcribe_words(av); w=captions.align_to_script(w, open(script).read().strip()); w=fix_brand(w)
    brolls=[]
    for clip,anchors,bef,aft in broll_specs:
        win=find_window(w, set(anchors), bef, aft)
        if win: brolls.append((clip, win[0], win[1]))
    sup=[(s-0.10, e+0.35) for _,s,e in brolls]
    ass=WORK/f"s{idx}.ass"; build_ass(w, str(ass), sup, hook_end=6.2 if hook else 0.0)
    print(f"  seg{idx}: {len(brolls)} b-rolls" + (" + push-in" if hook else ""))
    inputs=["-i",av]
    for clip,_,_ in brolls: inputs+=["-i",clip]
    pushin="" if not hook else ",zoompan=z='if(lte(on,240),1+0.0003*on,1.072)':d=1:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1920:fps=30"
    parts=[f"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30{pushin},setsar=1,subtitles={ass}[base]"]
    base="[base]"
    for j,(clip,s,e) in enumerate(brolls):
        dur=e-s
        parts.append(f"[{j+1}:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30,setsar=1,fade=t=out:st={dur-0.12:.2f}:d=0.12:alpha=1,setpts=PTS+{s:.2f}/TB[ov{j}]")
    for j,(clip,s,e) in enumerate(brolls):
        lbl="[v]" if j==len(brolls)-1 else f"[c{j}]"
        parts.append(f"{base}[ov{j}]overlay=0:0:enable='between(t,{s:.2f},{e:.2f})'{lbl}"); base=lbl
    if not brolls: parts[-1]=parts[-1].replace("[base]","[v]")
    out=str(WORK/f"seg{idx}.mp4")
    run(["ffmpeg","-y",*inputs,"-filter_complex",";".join(parts),"-map","[v]","-map","0:a",
         "-c:v","libx264","-preset","veryfast","-crf","19","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k","-r","30","-ar","44100",out])
    return out

def passthru(idx, src):
    out=str(WORK/f"seg{idx}.mp4")
    run(["ffmpeg","-y","-i",src,"-vf","scale=1080:1920,fps=30,setsar=1","-c:v","libx264","-preset","veryfast","-crf","19","-pix_fmt","yuv420p","-c:a","aac","-b:a","192k","-r","30","-ar","44100",out])
    return out

def norm2pass(src_audio, out_wav):
    j=subprocess.run(["ffmpeg","-i",src_audio,"-af","loudnorm=I=-14:TP=-1.5:LRA=11:print_format=json","-f","null","-"],capture_output=True,text=True).stderr
    m=json.loads(j[j.rfind("{"):j.rfind("}")+1])
    flt=f"loudnorm=I=-14:TP=-1.5:LRA=11:measured_I={m['input_i']}:measured_TP={m['input_tp']}:measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}:linear=true"
    run(["ffmpeg","-y","-i",src_audio,"-af",flt,"-ar","44100","-ac","2",out_wav])

TWEET=(f"{BASE}/brolls/bt_tweet.mp4", ["anthropic","conectó","telegram"], 0.5, 3.5)
TG=(f"{BASE}/brolls/bt_telegram.mp4", ["miles"], 1.0, 3.2)
CARD=(f"{BASE}/brolls/bt_card.mp4", ["virtud","problema","viendo"], 0.6, 2.8)

print("seg1 AV1 (hook+contexto+voz)..."); s1=process(1, f"{BASE}/avatar_av1.mp4", f"{BASE}/s_av1.txt", [TWEET], hook=True)
print("seg2 voz_waveform...");            s2=passthru(2, f"{BASE}/voz_waveform.mp4")
print("seg3 AV2 (pregunta)...");          s3=process(3, f"{BASE}/avatar_av2.mp4", f"{BASE}/s_av2.txt", [TG])
print("seg4 hero (giro)...");             s4=passthru(4, f"{BASE}/hero_waveform.mp4")
print("seg5 AV3 (angulos+posicion+CTA)..."); s5=process(5, f"{BASE}/avatar_av3.mp4", f"{BASE}/s_av3full.txt", [CARD])
segs=[s1,s2,s3,s4,s5]

print("audio 2-pass loudnorm por segmento...")
awavs=[]
for i,s in enumerate(segs):
    a=str(WORK/f"a{i}.wav"); norm2pass(s, a); awavs.append(a)
al=WORK/"al.txt"; al.write_text("".join(f"file '{a}'\n" for a in awavs))
run(["ffmpeg","-y","-f","concat","-safe","0","-i",str(al),"-c:a","aac","-b:a","192k",str(WORK/"full_audio.m4a")])

print("concat video + mux audio normalizado...")
cl=WORK/"vl.txt"; cl.write_text("".join(f"file '{s}'\n" for s in segs))
run(["ffmpeg","-y","-f","concat","-safe","0","-i",str(cl),"-c","copy",str(WORK/"vid.mp4")])
run(["ffmpeg","-y","-i",str(WORK/"vid.mp4"),"-i",str(WORK/"full_audio.m4a"),"-map","0:v","-map","1:a","-c:v","copy","-c:a","aac","-b:a","192k","-shortest","-movflags","+faststart",OUTF])
dur=subprocess.check_output(["ffprobe","-v","error","-show_entries","format=duration","-of","csv=p=0",OUTF]).decode().strip()
print(f"OK -> {OUTF} ({float(dur):.1f}s)")
