from pathlib import Path
import subprocess, json, glob
BASE=Path('/home/kevin/shared/video-packs/ia-tomo-control-V1-production')
V2=Path('/home/kevin/shared/video-packs/ia-tomo-control-V2-premium')
OUT=V2/'video'; TMP=V2/'tmp_build_v3_clean'; QA=V2/'qa'; MAN=V2/'manifests'; SHORT=V2/'short'
for d in [OUT,TMP,QA,MAN,SHORT]: d.mkdir(parents=True, exist_ok=True)

def first(pattern):
    xs=sorted(glob.glob(str(pattern)))
    return Path(xs[0]) if xs else None

def dur(p):
    return float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','csv=p=0',str(p)], text=True).strip())

def run(cmd, log):
    with open(log,'w') as f:
        p=subprocess.run(cmd, stdout=f, stderr=f)
    if p.returncode!=0:
        raise SystemExit(f'FAILED {log}: {cmd}')

avatar_map={
  1:'s01-hook-avatar-v2.mp4',
  2:'s02-mobile-avatar-v2.mp4',
  4:'s04-defense-avatar-v2.mp4',
  6:'s06-cyber-avatar-v2.mp4',
  9:'s09-close-avatar-v2.mp4',
}
scenes=[]
for i in range(1,10):
    sid=f's{i:02d}'
    audio=first(BASE/f'assets/runway-visual-voice/{sid}-voiceover-*.mp3') or first(BASE/f'assets/runway-gateway/{sid}-voiceover-*.mp3')
    visual=None  # V4 clean audit: avoid problematic Runway B-roll until MotionArray replaces it
    image=first(BASE/f'assets/image2-scenes/scene-{i:02d}-*.png')
    style=BASE/f'assets/styleframes-v2/styleframe-{i:02d}-measured-text-v2.png'
    avatar=V2/'assets/avatar-v2'/avatar_map[i] if i in avatar_map else None
    if avatar and not avatar.exists(): avatar=None
    scenes.append(dict(i=i,sid=sid,audio=audio,visual=visual,image=image,style=style if style.exists() else None,avatar=avatar))

concat_list=TMP/'concat-v3-clean.txt'
manifest=[]
with open(concat_list,'w') as cf:
    for s in scenes:
        parts=[]
        # 1) Avatar as its own fullscreen segment with its embedded synced audio. Never overlay on graphics.
        if s['avatar']:
            av_out=TMP/f'{s["sid"]}_avatar_fullsync.mp4'
            # Preserve native source proportion; crop centered to 16:9 without deformation. No PIP, no unrelated audio.
            vf='scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,setsar=1,format=yuv420p'
            run(['ffmpeg','-y','-i',str(s['avatar']),'-vf',vf,'-c:v','libx264','-preset','veryfast','-crf','18','-c:a','aac','-b:a','192k','-ar','48000','-ac','2',str(av_out)], TMP/f'{s["sid"]}_avatar_fullsync.log')
            parts.append(av_out)
        # 2) Clean styleframe intro with no avatar.
        if not s['audio']:
            raise SystemExit(f'missing scene voiceover {s["sid"]}')
        adur=dur(s['audio'])
        intro=min(4.0, max(2.5, adur*0.045)) if s['style'] else 0
        rem=max(0.1, adur-intro)
        if intro>0:
            intro_out=TMP/f'{s["sid"]}_style_clean.mp4'
            vf='scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=0x05070c,setsar=1,format=yuv420p'
            run(['ffmpeg','-y','-loop','1','-framerate','25','-t',f'{intro:.3f}','-i',str(s['style']),'-f','lavfi','-t',f'{intro:.3f}','-i','anullsrc=channel_layout=stereo:sample_rate=48000','-vf',vf,'-c:v','libx264','-preset','veryfast','-crf','18','-r','25','-c:a','aac','-b:a','96k','-shortest',str(intro_out)], TMP/f'{s["sid"]}_style_clean.log')
            parts.append(intro_out)
        # 3) Background visual/image with scene voiceover. No avatar overlay.
        main_out=TMP/f'{s["sid"]}_vo_clean.mp4'
        if s['visual']:
            vf='scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,setsar=1,fps=25,format=yuv420p'
            run(['ffmpeg','-y','-stream_loop','-1','-t',f'{rem:.3f}','-i',str(s['visual']),'-i',str(s['audio']),'-vf',vf,'-map','0:v','-map','1:a','-t',f'{rem:.3f}','-c:v','libx264','-preset','veryfast','-crf','20','-r','25','-c:a','aac','-b:a','192k','-ar','48000','-ac','2','-shortest',str(main_out)], TMP/f'{s["sid"]}_vo_clean.log')
        else:
            vf="scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,setsar=1,zoompan=z='min(zoom+0.00035,1.08)':d=1:s=1920x1080:fps=25,format=yuv420p"
            run(['ffmpeg','-y','-loop','1','-framerate','25','-t',f'{rem:.3f}','-i',str(s['image']),'-i',str(s['audio']),'-vf',vf,'-map','0:v','-map','1:a','-t',f'{rem:.3f}','-c:v','libx264','-preset','veryfast','-crf','20','-r','25','-c:a','aac','-b:a','192k','-ar','48000','-ac','2','-shortest',str(main_out)], TMP/f'{s["sid"]}_vo_clean.log')
        parts.append(main_out)
        # concat parts for scene
        scene_list=TMP/f'{s["sid"]}_parts.txt'
        scene_list.write_text(''.join(f"file '{p}'\n" for p in parts))
        scene_out=TMP/f'{s["sid"]}_scene_clean.mp4'
        run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(scene_list),'-c','copy',str(scene_out)], TMP/f'{s["sid"]}_scene_concat.log')
        cf.write(f"file '{scene_out}'\n")
        manifest.append({k:(str(v) if isinstance(v,Path) else v) for k,v in s.items()} | {'sceneFile':str(scene_out),'voiceoverDuration':adur,'avatarStandalone':bool(s['avatar'])})
long=OUT/'la-ia-ya-tomo-el-control-V4-clean-avatar-sync-image2-motionarray-pending.mp4'
run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(concat_list),'-c','copy',str(long)], TMP/'concat_long.log')
short=SHORT/'la-ia-ya-tomo-el-control-V4-clean-avatar-sync-image2-short-9x16-motionarray-pending.mp4'
# Use short from clean long. This is still an assembly short, no final premium claim.
vf='scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,format=yuv420p'
run(['ffmpeg','-y','-i',str(long),'-t','59','-vf',vf,'-c:v','libx264','-preset','veryfast','-crf','19','-c:a','aac','-b:a','192k','-ar','48000','-ac','2',str(short)], TMP/'short.log')
(MAN/'v3-clean-avatar-sync-manifest.json').write_text(json.dumps({'status':'v4_clean_image2_assembly_created_motionarray_pending','invalidates':'V2 assembly progress','reason':'avatar is standalone with own synced audio; Image2 backgrounds only; no PIP over graphics','long':str(long),'short':str(short),'scenes':manifest}, indent=2, ensure_ascii=False))
print(long)
print(short)
