#!/usr/bin/env python3
from __future__ import annotations
import datetime as dt
import hashlib
import json
import os
from pathlib import Path
import subprocess

ROOT=Path('/data/video-pipeline/HaTramAudio/project/016-Nguoi-Giu-Am-Thanh-Cuoi-Cung')
PROJECT=ROOT.name;RUN='run-20260720T233725Z-24504f17';OWNER='zoro';CANON='fae6fce617a14dc4f8b3bd52968cc688a456c45a60bc2ed7701ccd8164875e02'
TEXT='Các bạn đang nghe truyện được phát từ Hạ Trâm Audio, chúc các bạn có những giây phút nghe truyện vui vẻ. Hãy ủng hộ chúng tôi bằng cách like video và đăng ký kênh.'
ENDPOINT='http://192.168.40.32:7861/voice/ngoc-huyen-clone'
POSTER=ROOT/'image/normalized/intro-poster-1920x1080.png';VOICE=ROOT/'audio/intro-voice.wav';SILENCE=ROOT/'audio/intro-silence-2s.wav';FULL=ROOT/'audio/intro-full.wav';VIDEO=ROOT/'output/intro/intro.mp4';RECEIPT=ROOT/'log/intro-render.json'

def now():return dt.datetime.now(dt.timezone.utc).isoformat().replace('+00:00','Z')
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  while b:=f.read(8*1024*1024):h.update(b)
 return h.hexdigest()
def atomic(p,o):
 p.parent.mkdir(parents=True,exist_ok=True);t=p.with_suffix(p.suffix+'.tmp');t.write_text(json.dumps(o,ensure_ascii=False,indent=2)+'\n');os.replace(t,p)
def probe(p):
 r=subprocess.run(['ffprobe','-v','error','-show_entries','format=duration:stream=codec_name,sample_rate,channels,width,height','-of','json',str(p)],capture_output=True,text=True,check=True);return json.loads(r.stdout)
def guard():
 l=json.loads((ROOT/'.ownership-lock.json').read_text());assert (l['project_id'],l['run_id'],l['owner'],l['status'])==(PROJECT,RUN,OWNER,'main_session_pipeline')
 assert sha(ROOT/'story/story-canon.txt')==CANON
 lm=json.loads((ROOT/'image/layout-manifest.json').read_text());assert lm['status']=='completed' and lm['verified'] and sha(POSTER)==lm['normalized_assets']['intro']['sha256']
def main():
 guard();VOICE.parent.mkdir(parents=True,exist_ok=True);VIDEO.parent.mkdir(parents=True,exist_ok=True);(ROOT/'work/intro').mkdir(parents=True,exist_ok=True)
 text_sha=hashlib.sha256(TEXT.encode()).hexdigest();cfg={'style':'doc_truyen','speed':'1.0','denoise':'true'}
 if RECEIPT.exists() and VIDEO.exists():
  r=json.loads(RECEIPT.read_text());
  if r.get('status')=='completed' and r.get('verified') and r.get('video_sha256')==sha(VIDEO):print(json.dumps({'status':'completed_reused','video_sha256':r['video_sha256']}));return
 if VOICE.exists():raise RuntimeError('intro voice exists without terminal receipt; fail closed')
 r={'schema_version':1,'project_id':PROJECT,'run_id':RUN,'status':'requesting_voice','verified':False,'source_canon_sha256':CANON,'text':TEXT,'text_sha256':text_sha,'request_config':cfg,'poster_path':str(POSTER),'poster_sha256':sha(POSTER),'voice_requests':1,'created_at':now()};atomic(RECEIPT,r)
 tmp=ROOT/'work/intro/intro-voice.wav.part'
 q=subprocess.run(['curl','--fail-with-body','--silent','--show-error','--max-time','900','-X','POST',ENDPOINT,'-H','Content-Type: application/x-www-form-urlencoded','--data-urlencode',f'text={TEXT}','--data-urlencode','style=doc_truyen','--data-urlencode','speed=1.0','--data-urlencode','denoise=true','--output',str(tmp)],capture_output=True,text=True)
 if q.returncode: r.update(status='failed',error=(q.stderr or q.stdout)[-1000:],updated_at=now());atomic(RECEIPT,r);raise RuntimeError('intro TTS failed')
 os.replace(tmp,VOICE);vp=probe(VOICE);vd=float(vp['format']['duration']);assert vd>0
 subprocess.run(['ffmpeg','-v','error','-y','-f','lavfi','-i','anullsrc=r=48000:cl=mono','-t','2.000','-c:a','pcm_s16le',str(SILENCE)],check=True)
 concat=ROOT/'work/intro/concat.txt';concat.write_text(f"file '{VOICE.as_posix()}'\nfile '{SILENCE.as_posix()}'\n")
 subprocess.run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i',str(concat),'-ar','48000','-ac','1','-c:a','pcm_s16le',str(FULL)],check=True)
 fp=probe(FULL);fd=float(fp['format']['duration']);assert abs(fd-(vd+2.0))<0.1
 subprocess.run(['ffmpeg','-v','error','-y','-loop','1','-i',str(POSTER),'-i',str(FULL),'-t',f'{fd:.6f}','-r','30','-c:v','libx264','-preset','medium','-crf','18','-pix_fmt','yuv420p','-c:a','aac','-b:a','192k','-ar','48000','-ac','2','-movflags','+faststart',str(VIDEO)],check=True)
 out=probe(VIDEO);streams=out['streams'];vs=[x for x in streams if x.get('width')];aus=[x for x in streams if x.get('sample_rate')];dur=float(out['format']['duration']);assert len(vs)==1 and len(aus)==1 and (vs[0]['width'],vs[0]['height'])==(1920,1080) and abs(dur-fd)<=1/30+0.01
 r.update(status='completed',verified=True,voice_sha256=sha(VOICE),silence_sha256=sha(SILENCE),intro_audio_sha256=sha(FULL),voice_duration=vd,silence_duration=2.0,intro_audio_duration=fd,video_path=str(VIDEO),video_sha256=sha(VIDEO),video_bytes=VIDEO.stat().st_size,video_duration=dur,resolution=[1920,1080],updated_at=now());atomic(RECEIPT,r);print(json.dumps({'status':'completed','duration':dur,'video_sha256':r['video_sha256']}))
if __name__=='__main__':main()
