#!/usr/bin/env python3
from pathlib import Path
import sys,json,hashlib,re,wave,datetime,time
sys.path.insert(0,'/opt/OpenMontage')
from tools.audio.ngoc_huyen_clone_tts import NgocHuyenCloneTTS
P='gacmai_20260718_091744';R=Path('/data/video-pipeline/GacMaiAudio/project')/P;A=R/'artifacts';C=R/'assets/audio/chunks';C.mkdir(parents=True,exist_ok=True)
def sha(p):return hashlib.sha256(Path(p).read_bytes()).hexdigest()
story=A/'story_canonical.json';auth=A/'production_authorization.json';projection=A/'tts_projection.json'
assert story.exists() and auth.exists() and projection.exists()
S=sha(story);az=json.loads(auth.read_text());proj=json.loads(projection.read_text())
assert az.get('status')=='passed' and az.get('tts_authorized') is True and az.get('story_sha256')==S
assert proj.get('status')=='passed' and proj.get('story_sha256')==S and proj.get('segment_count')==144
intro='Cảm ơn các bạn đã nghe truyện từ Gác Mái Audio. Chúc các bạn có thời gian nghe truyện vui vẻ.'
units=[('INTRO',intro)]
for seg in proj['segments']:
 sentences=[x.strip() for x in re.split(r'(?<=[.!?…])\s+',seg['text']) if x.strip()];pieces=[]
 for s in sentences:
  while len(s)>260:
   cuts=[m.end() for m in re.finditer(r'[,;:]\s+',s[:261])];cut=cuts[-1] if cuts else s[:261].rfind(' ')+1
   if cut<=0:raise SystemExit(f'unsplittable:{seg["id"]}')
   pieces.append(s[:cut].strip());s=s[cut:].strip()
  if s:pieces.append(s)
 assert ' '.join(pieces)==seg['text']
 buf='';n=0
 for s in pieces:
  if buf and len(buf)+1+len(s)>260:units.append((f'{seg["id"]}_{n:02d}',buf));n+=1;buf=s
  else:buf=(buf+' '+s).strip()
 if buf:units.append((f'{seg["id"]}_{n:02d}',buf))
tts=NgocHuyenCloneTTS();manifest=[]
for idx,(uid,text) in enumerate(units):
 out=C/f'{idx:04d}_{uid}.wav';th=hashlib.sha256(text.encode()).hexdigest()
 ok=False
 if out.exists():
  try:
   with wave.open(str(out),'rb') as w:ok=w.getnchannels()==1 and w.getsampwidth()==2 and w.getframerate()==48000 and w.getnframes()>0
  except:ok=False
 if not ok:
  out.unlink(missing_ok=True);err=None
  for attempt in range(1,4):
   try:
    r=tts.execute({'text':text,'style':'doc_truyen','speed':0.95,'denoise':True,'output_path':str(out),'timeout':180})
    if not r.success or not out.exists():raise RuntimeError(r.error or 'provider output missing')
    with wave.open(str(out),'rb') as w:ok=w.getnchannels()==1 and w.getsampwidth()==2 and w.getframerate()==48000 and w.getnframes()>0
    if not ok:raise RuntimeError('invalid PCM')
    err=None;break
   except Exception as e:err=str(e);out.unlink(missing_ok=True);time.sleep(attempt*2)
  if err:raise SystemExit(f'chunk {idx} failed:{err}')
 manifest.append({'index':idx,'id':uid,'text_sha256':th,'audio_sha256':sha(out),'path':str(out)})
 partial={'status':'in_progress','story_sha256':S,'projection_sha256':sha(projection),'planned_count':len(units),'completed_count':len(manifest),'chunks':manifest};(A/'tts_manifest.partial.json').write_text(json.dumps(partial,ensure_ascii=False,indent=2)+'\n')
final={'status':'passed','project_id':P,'story_sha256':S,'projection_sha256':sha(projection),'voice':'Ngoc-Huyen-Clone','style':'doc_truyen','speed':0.95,'denoise':True,'planned_count':len(units),'completed_count':len(manifest),'chunks':manifest,'created_at':datetime.datetime.now(datetime.timezone.utc).isoformat()};p=A/'tts_manifest.json';p.write_text(json.dumps(final,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'status':'passed','chunks':len(units),'manifest_sha256':sha(p)}))
