#!/usr/bin/env python3
import hashlib,json,os,subprocess
from datetime import datetime,timezone
from pathlib import Path
P=Path(__file__).resolve().parents[1];LEFT=P/'image/left-panel-master.png';RIGHT=P/'image/right-panel-master.png';QA=P/'log/image-visual-qa.json';OUT=P/'image/layout-1920x1080-r2.png';REC=P/'image/layout-manifest-r2.json'
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  for b in iter(lambda:f.read(8*1024*1024),b''):h.update(b)
 return h.hexdigest()
def probe(p):return json.loads(subprocess.check_output(['ffprobe','-v','error','-show_entries','stream=codec_name,width,height,pix_fmt','-of','json',str(p)]))['streams'][0]
for p in [LEFT,RIGHT,QA]:
 if not p.is_file():raise SystemExit(f'missing {p}')
tmp=OUT.with_suffix('.part.png');filtergraph='[0:v]scale=656:1080:force_original_aspect_ratio=decrease:flags=lanczos,pad=656:1080:(ow-iw)/2:(oh-ih)/2:color=0x101820[left];[1:v]scale=656:1080:force_original_aspect_ratio=decrease:flags=lanczos,pad=656:1080:(ow-iw)/2:(oh-ih)/2:color=0x101820[right];color=c=0x101820:s=608x1080:r=1[center];[left][center][right]hstack=inputs=3,format=rgb24[out]';subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-i',str(LEFT),'-i',str(RIGHT),'-filter_complex',filtergraph,'-map','[out]','-frames:v','1','-threads','1',str(tmp)],check=True);os.replace(tmp,OUT);op=probe(OUT);assert op['width']==1920 and op['height']==1080
rec={'version':3,'migration_id':'016-artwork-contract-r2','verified':True,'status':'completed','builder_location':'agent_local','builder_tool':{'name':'FFmpeg','filters':'scale(force_original_aspect_ratio=decrease,flags=lanczos)+pad+color+hstack','pixel_format':'rgb24','background_rgb':[16,24,32]},'operation':'local static-image normalize/resample/pad/hstack only; no crop; no text overlay','text_overlay_applied':False,'video_created_or_modified':False,'remote_layout_build_forbidden':True,'source_canon_sha256':sha(P/'story/story-canon.txt'),'workflow_copy_authority_sha256':sha(P/'script/workflow-copy-authority.json'),'image_visual_qa_receipt':'log/image-visual-qa.json','image_visual_qa_sha256':sha(QA),'source_panels':{'left':{'path':str(LEFT.relative_to(P)),'sha256':sha(LEFT),'probe':probe(LEFT)},'right':{'path':str(RIGHT.relative_to(P)),'sha256':sha(RIGHT),'probe':probe(RIGHT)}},'normalized_panels':{'left':{'path':str(LEFT.relative_to(P)),'sha256':sha(LEFT)},'right':{'path':str(RIGHT.relative_to(P)),'sha256':sha(RIGHT)}},'output':{'path':str(OUT.relative_to(P)),'sha256':sha(OUT),'bytes':OUT.stat().st_size,'probe':op},'canvas':{'width':1920,'height':1080},'regions':{'left':{'x':0,'y':0,'width':656,'height':1080},'center':{'x':656,'y':0,'width':608,'height':1080,'clean':True},'right':{'x':1264,'y':0,'width':656,'height':1080}},'visual_qa':{'verified':False,'status':'pending'},'built_at':datetime.now(timezone.utc).isoformat()};REC.write_text(json.dumps(rec,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'verified':True,'output':str(OUT),'sha256':rec['output']['sha256'],'probe':op},ensure_ascii=False))
      