#!/usr/bin/env python3
from pathlib import Path
import hashlib,json,re,urllib.parse,urllib.request,subprocess,datetime
R=Path('/data/video-pipeline/HaTramAudio/project/027-Nguoi-Thua-Ke-Khong-Nhan-Chiec-Ghe-Danh-San');W=R/'work/tts';O=R/'audio';END='http://192.168.40.32:7861/voice/ngoc-huyen-clone';CANON='4db101987ecb4d1447cb9c898eba5785db54d614f0c32b16a6b7bdb6cbfbd306'
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  for b in iter(lambda:f.read(1<<20),b''):h.update(b)
 return h.hexdigest()
def atomic(p,o):
 q=p.with_suffix(p.suffix+'.tmp');q.write_text(json.dumps(o,ensure_ascii=False,indent=2)+'\n');q.replace(p)
def split_small(text,limit=1400):
 ps=[x for x in text.split('\n\n') if x];out=[];cur=''
 for p in ps:
  if len(cur)+len(p)+2<=limit:cur=(cur+'\n\n'+p).strip()
  else:
   if cur:out.append(cur)
   if len(p)<=limit:cur=p
   else:
    ss=re.split(r'(?<=[.!?])\s+',p);cur=''
    for s in ss:
     if len(cur)+len(s)+1<=limit:cur=(cur+' '+s).strip()
     else:
      if cur:out.append(cur)
      cur=s
 if cur:out.append(cur)
 return out
src=(R/'script/spoken-narration.txt').read_text();paras=[x.strip() for x in re.split(r'\n\s*\n',src) if x.strip()];chunks=[];cur=''
for p in paras:
 if len(cur)+len(p)+2<=2800:cur=(cur+'\n\n'+p).strip()
 else:
  if cur:chunks.append(cur)
  cur=p
if cur:chunks.append(cur)
hot=W/'hot-manifest.json';m=json.loads(hot.read_text());completed={x['index']:x for x in m['segments'] if x.get('status')=='completed'}
for idx in range(9,len(chunks)+1):
 text=chunks[idx-1];th=hashlib.sha256(text.encode()).hexdigest();target=O/f'segment-{idx:04d}.wav'
 if idx in completed and target.exists() and completed[idx]['text_sha256']==th and completed[idx]['artifact_sha256']==sha(target):continue
 subs=split_small(text);parts=[]
 for j,sub in enumerate(subs,1):
  out=W/f'recovery-{idx:04d}-{j}.wav';rp=W/f'recovery-{idx:04d}-{j}.json';sth=hashlib.sha256(sub.encode()).hexdigest()
  if rp.exists() and out.exists():
   rd=json.loads(rp.read_text())
   if rd.get('verified') and rd.get('text_sha256')==sth and rd.get('artifact_sha256')==sha(out):parts.append(out);continue
  rec={'schema_version':1,'project_id':'027','run_id':'run-20260722T063859Z-aa8eca81','segment_index':idx,'subpart':j,'status':'creating','source_canon_sha256':CANON,'parent_text_sha256':th,'text_sha256':sth,'created_at':datetime.datetime.now(datetime.timezone.utc).isoformat()};atomic(rp,rec)
  data=urllib.parse.urlencode({'text':sub,'style':'doc_truyen','speed':'1.0','denoise':'true'}).encode();req=urllib.request.Request(END,data=data,method='POST',headers={'Content-Type':'application/x-www-form-urlencoded'});tmp=out.with_suffix('.tmp.wav')
  with urllib.request.urlopen(req,timeout=600) as resp:tmp.write_bytes(resp.read())
  subprocess.run(['ffprobe','-v','error','-show_entries','format=duration','-of','json',str(tmp)],check=True,stdout=subprocess.DEVNULL);tmp.replace(out);rec.update({'status':'completed','verified':True,'artifact_sha256':sha(out),'bytes':out.stat().st_size,'completed_at':datetime.datetime.now(datetime.timezone.utc).isoformat()});atomic(rp,rec);parts.append(out)
 lst=W/f'recovery-{idx:04d}-concat.txt';lst.write_text(''.join("file '%s'\n"%p for p in parts));tmp=O/f'segment-{idx:04d}.tmp.wav';subprocess.run(['ffmpeg','-y','-v','error','-f','concat','-safe','0','-i',str(lst),'-c','copy',str(tmp)],check=True);pr=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration:stream=codec_type,codec_name,sample_rate,channels','-of','json',str(tmp)]));tmp.replace(target);seg={'index':idx,'status':'completed','text_sha256':th,'path':str(target.relative_to(R)),'size':target.stat().st_size,'artifact_sha256':sha(target),'probe':pr,'recovery':'small_subparts_concat'};completed[idx]=seg;m['segments']=[completed[k] for k in sorted(completed)];atomic(hot,m);print(json.dumps({'segment':idx,'status':'completed','subparts':len(parts)},ensure_ascii=False),flush=True)
print(json.dumps({'status':'remaining_completed','segments':len(completed)},ensure_ascii=False))
