from pathlib import Path
import json, os, hashlib, subprocess, urllib.parse, urllib.request, datetime, wave, time
root=Path(__file__).resolve().parents[1]; plan_path=root/'script/tts-plan.json'; receipt_path=root/'log/tts.json'
def atomic_json(path,obj):
 tmp=Path(str(path)+'.tmp'); tmp.write_text(json.dumps(obj,ensure_ascii=False,indent=2)+'\n'); os.replace(tmp,path)
def sha(path):
 h=hashlib.sha256()
 with path.open('rb') as f:
  for b in iter(lambda:f.read(8*1024*1024),b''): h.update(b)
 return h.hexdigest()
plan=json.loads(plan_path.read_text()); completed=[]
for seg in plan['segments']:
 out=Path(seg['output_path']); part=Path(str(out)+'.part'); out.parent.mkdir(parents=True,exist_ok=True)
 if out.exists() and out.stat().st_size>44:
  try:
   with wave.open(str(out),'rb') as w: dur=w.getnframes()/w.getframerate(); assert dur>0
   seg.update(status='completed',artifact_sha256=sha(out),duration_seconds=dur); atomic_json(plan_path,plan); continue
  except Exception: pass
 data=urllib.parse.urlencode({'text':seg['text'],'voice':plan['voice']}).encode()
 assert hashlib.sha256(data).hexdigest()==seg['request_sha256']
 last=None
 for attempt in range(1,4):
  try:
   req=urllib.request.Request(plan['endpoint'],data=data,headers={'Content-Type':plan['content_type']},method='POST')
   with urllib.request.urlopen(req,timeout=600) as r:
    ctype=r.headers.get('Content-Type',''); payload=r.read()
   if not (payload[:4]==b'RIFF' and payload[8:12]==b'WAVE'): raise RuntimeError('non-WAV response '+ctype)
   part.write_bytes(payload)
   with wave.open(str(part),'rb') as w:
    dur=w.getnframes()/w.getframerate(); rate=w.getframerate(); channels=w.getnchannels(); assert dur>0
   os.replace(part,out); seg.update(status='completed',artifact_sha256=sha(out),duration_seconds=dur,sample_rate=rate,channels=channels,attempts=attempt,content_type=ctype); atomic_json(plan_path,plan); break
  except Exception as e:
   last=str(e); seg.update(status='retrying',attempts=attempt,last_error=last); atomic_json(plan_path,plan)
   if attempt<3: time.sleep(2**attempt)
 else:
  seg.update(status='failed',last_error=last); atomic_json(plan_path,plan); raise RuntimeError(f"segment {seg['index']} failed: {last}")
# Concatenate verified PCM WAVs using local ffmpeg audio-only.
concat=root/'audio/segments/concat.txt'; concat.write_text(''.join("file '"+str(Path(s['output_path']))+"'\n" for s in plan['segments']))
final=root/'audio/story-full.wav'; part=Path(str(final)+'.part.wav')
subprocess.run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(concat),'-c:a','pcm_s16le',str(part)],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
probe=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-show_entries','stream=index,codec_type,codec_name,sample_rate,channels','-of','json',str(part)]))
os.replace(part,final); artifact=sha(final)
receipt={'status':'completed','verified':True,'provider':'piper-wrapper','base_url':'http://192.168.40.33:7862','endpoint':'/tts','voice':plan['voice'],'input_path':plan['input_path'],'input_text_sha256':plan['input_text_sha256'],'segment_count':len(plan['segments']),'output_path':str(final),'artifact_sha256':artifact,'artifact_bytes':final.stat().st_size,'duration_seconds':float(probe['format']['duration']),'probe':probe,'health_checked_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'created_at':datetime.datetime.now(datetime.timezone.utc).isoformat()}
atomic_json(receipt_path,receipt); print(json.dumps(receipt,ensure_ascii=False))
