#!/usr/bin/env python3
from __future__ import annotations
import datetime as dt,hashlib,json,os,subprocess,urllib.parse,urllib.request
from pathlib import Path
ROOT=Path('/data/video-pipeline/HaTramAudio/project/018-Hon-Uoc-Khong-Duoc-Phep-Yeu');PROJECT=ROOT.name;RUN='run-20260721T040505Z-cd9f944a';OWNER='zoro';CANON='d12e1733f81bf173b5db789b0028a0be478d0d2e565c05a91e236c95cd33bd78';POSTER='e9da2b4a8ed90e9f947392bc8f7099ecec080e0d567769b8cff8477c04dd5722';ENDPOINT='http://192.168.40.32:7861/voice/ngoc-huyen-clone';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.'
VOICE=ROOT/'audio/intro-voice.wav';SIL=ROOT/'audio/intro-silence-2s.wav';FULL=ROOT/'audio/intro-full.wav';OUT=ROOT/'output/intro/intro.mp4';INTENT=ROOT/'log/intro-tts-intent.json';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:
  for b in iter(lambda:f.read(8*1024*1024),b''):h.update(b)
 return h.hexdigest()
def atomic(p,v):p.parent.mkdir(parents=True,exist_ok=True);q=p.with_suffix(p.suffix+'.tmp');q.write_text(json.dumps(v,ensure_ascii=False,indent=2)+'\n');os.replace(q,p)
def duration(p):return float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','default=nw=1:nk=1',str(p)],text=True))
def guard():
 x=json.loads((ROOT/'.ownership-lock.json').read_text());assert (x['project_id'],x['run_id'],x['owner'],x['status'])==(PROJECT,RUN,OWNER,'main_session_pipeline');assert sha(ROOT/'story/story-canon.txt')==CANON and sha(ROOT/'image/normalized/intro-poster-1920x1080.png')==POSTER

def main():
 guard();assert not INTENT.exists() and not VOICE.exists() and not OUT.exists();intent={'schema_version':1,'project_id':PROJECT,'run_id':RUN,'status':'submitting','source_canon_sha256':CANON,'source_poster_sha256':POSTER,'text_sha256':hashlib.sha256(TEXT.encode()).hexdigest(),'request_config':{'style':'doc_truyen','speed':'1.0','denoise':'true'},'voice_test':False,'created_at':now()};atomic(INTENT,intent)
 data=urllib.parse.urlencode({'text':TEXT,'style':'doc_truyen','speed':'1.0','denoise':'true'}).encode();req=urllib.request.Request(ENDPOINT,data=data,headers={'Content-Type':'application/x-www-form-urlencoded'},method='POST');tmp=VOICE.with_name('intro-voice.tmp.wav')
 with urllib.request.urlopen(req,timeout=1200) as r,tmp.open('wb') as f:
  assert r.status==200
  while True:
   b=r.read(1024*1024)
   if not b:break
   f.write(b)
 os.replace(tmp,VOICE);vd=duration(VOICE);assert vd>0
 subprocess.run(['ffmpeg','-y','-v','error','-f','lavfi','-i','anullsrc=r=48000:cl=mono','-t','2.000','-c:a','pcm_s16le',str(SIL)],check=True);assert abs(duration(SIL)-2.0)<0.001
 concat=ROOT/'work/render/intro-concat.txt';concat.write_text(f"file '{VOICE}'\nfile '{SIL}'\n");tmpfull=FULL.with_name('intro-full.tmp.wav');subprocess.run(['ffmpeg','-y','-v','error','-f','concat','-safe','0','-i',str(concat),'-c','copy',str(tmpfull)],check=True);os.replace(tmpfull,FULL);fd=duration(FULL);assert abs(fd-(vd+2.0))<0.03
 OUT.parent.mkdir(parents=True,exist_ok=True);tmpout=OUT.with_name('intro.tmp.mp4');subprocess.run(['ffmpeg','-y','-v','error','-loop','1','-i',str(ROOT/'image/normalized/intro-poster-1920x1080.png'),'-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','1','-movflags','+faststart',str(tmpout)],check=True);os.replace(tmpout,OUT);od=duration(OUT);assert abs(od-fd)<=1/30+0.01
 intent.update({'status':'completed','voice_sha256':sha(VOICE),'completed_at':now()});atomic(INTENT,intent);receipt={'schema_version':1,'project_id':PROJECT,'run_id':RUN,'status':'awaiting_visual_qa','verified':False,'source_canon_sha256':CANON,'source_poster_sha256':POSTER,'intro_text_sha256':intent['text_sha256'],'voice_path':str(VOICE),'voice_sha256':sha(VOICE),'voice_duration':vd,'silence_path':str(SIL),'silence_sha256':sha(SIL),'silence_duration':duration(SIL),'full_audio_path':str(FULL),'full_audio_sha256':sha(FULL),'full_audio_duration':fd,'output_path':str(OUT),'output_sha256':sha(OUT),'output_bytes':OUT.stat().st_size,'output_duration':od,'duration_delta':abs(od-fd),'video_codec':'h264','audio_codec':'aac','resolution':'1920x1080','created_at':now()};atomic(RECEIPT,receipt);print(json.dumps({'status':receipt['status'],'duration':od,'bytes':OUT.stat().st_size,'sha256':receipt['output_sha256']}))
if __name__=='__main__':main()
