#!/usr/bin/env python3
from pathlib import Path
from array import array
import datetime,hashlib,json,os,subprocess,sys,wave
ROOT=Path('/data/video-pipeline/HaTramAudio/project/025-Nguoi-Giu-Cho-Cuoi-Cung');RUN='run-20260721T232244Z-a0837ee4';OWNER='Levy';OUT=ROOT/'audio/story-full.wav';MAN=ROOT/'script/tts-manifest.json'
def now():return datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00','Z')
def sha(p):
 h=hashlib.sha256()
 with p.open('rb') as f:
  for b in iter(lambda:f.read(8*1024*1024),b''):h.update(b)
 return h.hexdigest()
def guard():
 d=json.loads((ROOT/'.ownership-lock.json').read_text())
 for k,v in {'project_id':ROOT.name,'run_id':RUN,'owner':OWNER}.items():
  if d.get(k)!=v:raise RuntimeError('ownership mismatch '+k)
def clips(p):
 count=total=0
 with wave.open(str(p),'rb') as w:
  if w.getsampwidth()!=2 or w.getframerate()!=48000 or w.getnchannels()!=1:raise RuntimeError('unexpected WAV format')
  while raw:=w.readframes(48000*10):
   a=array('h');a.frombytes(raw)
   if sys.byteorder!='little':a.byteswap()
   total+=len(a);count+=sum(1 for x in a if x in (-32768,32767))
 return total,count
def atomic(p,d):
 guard();t=p.with_name('.'+p.name+'.tmp');t.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n');os.replace(t,p)
def main():
 guard();m=json.loads(MAN.read_text())
 if m.get('status')!='completed' or m.get('verified') is not True or m.get('run_id')!=RUN:raise RuntimeError('TTS manifest not current/completed')
 if m.get('technical_correction'):raise RuntimeError('technical correction already exists; refusing second correction')
 if not OUT.is_file() or sha(OUT)!=m.get('output_sha256'):raise RuntimeError('TTS output hash drift')
 sample_count,pre_clips=clips(OUT)
 if pre_clips<=0:raise RuntimeError('no clipped samples; correction is not authorized')
 pre_hash=sha(OUT);tmp=OUT.with_name('.story-full.gain-corrected.tmp.wav');tmp.unlink(missing_ok=True)
 subprocess.check_call(['ffmpeg','-hide_banner','-loglevel','error','-y','-i',str(OUT),'-af','volume=-1dB','-ar','48000','-ac','1','-c:a','pcm_s16le',str(tmp)])
 post_sample_count,post_clips=clips(tmp)
 if post_sample_count!=sample_count or post_clips!=0:tmp.unlink(missing_ok=True);raise RuntimeError('uniform gain correction did not produce zero-clipping equivalent-length PCM')
 guard();os.replace(tmp,OUT);post_hash=sha(OUT)
 correction={'type':'uniform_gain','filter':'volume=-1dB','authorized_by':'direct PCM16 scan on current production output','pre_correction_sha256':pre_hash,'pre_correction_clipped_samples':pre_clips,'post_correction_sha256':post_hash,'post_correction_clipped_samples':post_clips,'sample_count':sample_count,'applied_at':now()}
 m['technical_correction']=correction;m['output_sha256']=post_hash;m['output_bytes']=OUT.stat().st_size;m['updated_at']=now();atomic(MAN,m)
 print(json.dumps({'status':'completed','verified':True,'pre_clipped_samples':pre_clips,'post_clipped_samples':post_clips,'post_sha256':post_hash}))
if __name__=='__main__':main()
