from pathlib import Path
import subprocess, json

P = Path(__file__).resolve().parent
V5 = P.parent / 'v5'
BASE = P.parents[1] / 'there-while-not-there-v5.mp4'
OUT = P / 'there-while-not-there-final.mp4'
SEEDANCE = V5 / 'clips_seedance_seedance_v5frame.mp4'
timeline = json.loads((V5 / 'timeline.json').read_text())
scene = next(s for s in timeline['scenes'] if s['source_scene'] == 6)

def run(args):
    r = subprocess.run([str(a) for a in args], capture_output=True, text=True, check=True)
    return r.stdout

def probe(path):
    return json.loads(run(['ffprobe','-v','error','-show_streams','-show_format','-of','json',path]))

replacement = P / 'schematic_seedance.mp4'
run(['ffmpeg','-v','error','-y','-i',SEEDANCE,'-vf','fps=24,scale=1920:1080,setsar=1,setpts=PTS-STARTPTS','-frames:v',scene['frames'],'-an','-c:v','libx264','-threads','1','-preset','fast','-crf','18','-pix_fmt','yuv420p',replacement])
assert int(probe(replacement)['streams'][0]['nb_frames']) == scene['frames']
paths = [replacement if s['source_scene'] == 6 else Path(s['segment']) for s in timeline['scenes']]
assert all(p.is_file() for p in paths)
(P / 'concat.txt').write_text(''.join("file '" + str(p) + "'\n" for p in paths))
run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i',P/'concat.txt','-i',BASE,'-map','0:v:0','-map','1:a:0','-c','copy','-movflags','+faststart',OUT])
result = probe(OUT)
video = next(s for s in result['streams'] if s['codec_type']=='video')
audio = next(s for s in result['streams'] if s['codec_type']=='audio')
assert int(video['nb_frames']) == timeline['total_frames']
assert abs(float(video['duration']) - timeline['duration']) < .001
assert abs(float(audio['duration']) - timeline['duration']) < .05

def audio_hash(path):
    return run(['ffmpeg','-v','error','-i',path,'-map','0:a:0','-c','copy','-f','hash','-hash','sha256','-']).strip()

h1,h2=audio_hash(BASE),audio_hash(OUT)
assert h1 == h2, 'The final must preserve the existing mastered audio exactly.'

def frame_hashes(path):
    lines=run(['ffmpeg','-v','error','-threads','1','-i',path,'-map','0:v:0','-f','framemd5','-']).splitlines()
    return [l.rsplit(',',1)[-1].strip() for l in lines if l and not l.startswith('#')]

before,after=frame_hashes(BASE),frame_hashes(OUT)
a=scene['start_frame'];b=a+scene['frames']
assert len(before)==len(after)==timeline['total_frames']
assert before[:a] == after[:a] and before[b:] == after[b:], 'A shot outside the replacement range changed.'
report={'duration':float(video['duration']),'width':video['width'],'height':video['height'],'fps':video['r_frame_rate'],'frames':int(video['nb_frames']),'audio_bitstream_unchanged':True,'audio_sha256':h2,'all_other_shots_frame_identical':True,'replacement_start':a/24,'replacement_end':b/24,'seedance_input':str(SEEDANCE),'output':str(OUT)}
(P/'verification.json').write_text(json.dumps(report,indent=2))
for t,name in [(35.70,'before'),(36.3,'entry'),(42,'middle'),(48.3,'exit'),(48.65,'after'),(64.5,'end')]:
    run(['ffmpeg','-v','error','-y','-ss',t,'-i',OUT,'-frames:v','1','-vf','scale=960:540',P/f'check_{name}.jpg'])
run(['ffmpeg','-v','error','-y','-i',P/'check_entry.jpg','-i',P/'check_middle.jpg','-i',P/'check_exit.jpg','-i',P/'check_end.jpg','-filter_complex','[0][1]hstack[a];[2][3]hstack[b];[a][b]vstack',P/'final_check.jpg'])
print(json.dumps(report,indent=2))
