#!/usr/bin/env python3
from pathlib import Path
import datetime as dt,hashlib,json,subprocess,urllib.parse,urllib.request,wave
ROOT=Path('/data/video-pipeline/HaTramAudio/project/021-Ngay-Toi-Bi-Gach-Ten-Khoi-Gia-Pha');RUN='run-20260721T131911Z-ac5d64d1';CANON='bb80e97d9adcb0427ea3828449858445323db228cca38f37cdf0446fffefdd6f';POSTER=ROOT/'image/normalized/intro-poster-1920x1080.png';POSTER_HASH='37c64f54f8485a4ea5fca2b0ca63530715cc428f4c27226d3056df3c77a2a150';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.'
def now():return dt.datetime.now(dt.timezone.utc).isoformat().replace('+00:00','Z')
def sha(p):
 h=hashlib.sha256()
 with open(p,'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');t.replace(p)
def duration(p):
 return float(subprocess.run(['ffprobe','-v','error','-show_entries','format=duration','-of','default=nw=1:nk=1',str(p)],capture_output=True,text=True,check=True).stdout.strip())
def main():
 lock=json.loads((ROOT/'.ownership-lock.json').read_text());assert lock['run_id']==RUN and lock['owner']=='zoro' and lock['status']=='main_session_pipeline';assert sha(ROOT/'story/story-canon.txt')==CANON and sha(POSTER)==POSTER_HASH
 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';attempt=ROOT/'log/intro-tts-attempt.json'
 intent={'schema_version':1,'project_id':ROOT.name,'run_id':RUN,'status':'creating','source_canon_sha256':CANON,'text':TEXT,'text_sha256':hashlib.sha256(TEXT.encode()).hexdigest(),'request_config':{'style':'doc_truyen','denoise':'true','speed':'1.0'},'output_path':str(voice),'created_at':now()};atomic(attempt,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')
 temp=voice.with_suffix('.wav.part')
 with urllib.request.urlopen(req,timeout=900) as r,open(temp,'wb') as f:
  assert r.status==200
  while b:=r.read(1024*1024):f.write(b)
 temp.replace(voice);assert voice.stat().st_size>0
 subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-f','lavfi','-i','anullsrc=r=48000:cl=mono','-t','2.000','-c:a','pcm_s16le',str(silence)],check=True)
 concat=ROOT/'work/render/intro-concat.txt';concat.write_text(f"file '{voice}'\nfile '{silence}'\n")
 subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-f','concat','-safe','0','-i',str(concat),'-ac','1','-ar','48000','-c:a','pcm_s16le',str(full)],check=True)
 vd=duration(full);video.parent.mkdir(parents=True,exist_ok=True);subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y','-loop','1','-i',str(POSTER),'-i',str(full),'-t',f'{vd:.6f}','-r','30','-c:v','h264_nvenc','-preset','p4','-cq','21','-pix_fmt','yuv420p','-c:a','aac','-b:a','192k','-movflags','+faststart',str(video)],check=True)
 video_d=duration(video);assert abs(video_d-vd)<=1/30+0.01
 intent.update({'status':'completed','verified':True,'voice_sha256':sha(voice),'voice_duration':duration(voice),'silence_sha256':sha(silence),'silence_duration':duration(silence),'completed_at':now()});atomic(attempt,intent)
 receipt={'schema_version':1,'project_id':ROOT.name,'run_id':RUN,'status':'completed','verified':True,'source_canon_sha256':CANON,'poster_path':str(POSTER),'poster_sha256':POSTER_HASH,'intro_text':TEXT,'voice_path':str(voice),'voice_sha256':sha(voice),'silence_path':str(silence),'silence_duration_seconds':duration(silence),'audio_path':str(full),'audio_sha256':sha(full),'audio_duration_seconds':vd,'output_path':str(video),'output_sha256':sha(video),'output_size_bytes':video.stat().st_size,'output_duration_seconds':video_d,'resolution':[1920,1080],'fps':30,'created_at':now()};atomic(ROOT/'log/intro-render.json',receipt);print(json.dumps({'status':'completed','verified':True,'duration_seconds':video_d,'bytes':video.stat().st_size,'sha256':receipt['output_sha256']},ensure_ascii=False))
if __name__=='__main__':main()
