#!/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_20260717_184527';R=Path('/data/video-pipeline/GacMaiAudio/project')/P;A=R/'artifacts';C=R/'assets/audio/chunks';C.mkdir(parents=True,exist_ok=True)
S='ad87b29af9ec8ab3fd6fb1a7ee8ef5d83062f56bb8d4d5cf8dbdc709f8dee666';proj=json.loads((A/'tts_projection.json').read_text());lock=json.loads((A/'tts_spend_lock.json').read_text());story=A/'story_package_canonical.json'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
assert sha(story)==S and proj['story_sha256']==S and lock['tts_locked'] is False and lock['authorized_story_sha256']==S
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()
 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
   if ok:manifest.append({'index':idx,'id':uid,'text_sha256':th,'audio_sha256':sha(out),'path':str(out)});continue
  except:pass
  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:raise RuntimeError(r.error or 'provider returned unsuccessful result')
   if not out.exists():raise RuntimeError('provider succeeded without output file')
   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')
   manifest.append({'index':idx,'id':uid,'text_sha256':th,'audio_sha256':sha(out),'path':str(out)});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}')
 (A/'tts_manifest.partial.json').write_text(json.dumps({'status':'in_progress','story_sha256':S,'planned_count':len(units),'completed_count':len(manifest),'chunks':manifest},ensure_ascii=False,indent=2)+'\n')
final={'status':'passed','story_sha256':S,'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()};(A/'tts_manifest.json').write_text(json.dumps(final,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'status':'passed','chunks':len(units),'manifest_sha256':sha(A/'tts_manifest.json')}))
