"""Save ElevenLabs timed speech as MP3 + SRT.

Set ELEVENLABS_API_KEY and VOICE_ID in your environment. MODEL_ID is optional.
API generation consumes your provider allowance. This script never retries.
--from-json tests conversion offline; it makes no network request.
"""
import argparse,base64,json,math,os,sys
from pathlib import Path
from urllib.request import Request,urlopen
from urllib.error import HTTPError,URLError
from urllib.parse import quote

def subtitle_cues(response,max_chars=22):
 alignment=response.get('normalized_alignment') or response.get('alignment')
 if not alignment:raise ValueError('The response has no alignment. Audio can be saved, but subtitle timing is unavailable.')
 chars=alignment['characters'];starts=alignment['character_start_times_seconds'];ends=alignment['character_end_times_seconds']
 if not (len(chars)==len(starts)==len(ends)):raise ValueError('Alignment array lengths differ.')
 cues=[];text='';begin=None;previous=0.0
 for char,start,end in zip(chars,starts,ends):
  if not isinstance(char,str):raise ValueError('Alignment characters must be strings.')
  start,end=float(start),float(end)
  if not (math.isfinite(start) and math.isfinite(end) and 0<=start<=end):raise ValueError('Invalid alignment time.')
  if start+0.001<previous:raise ValueError('Alignment times are not monotonic.')
  if text and start-previous>0.65:cues.append((begin,previous,text.strip()));text='';begin=None
  if begin is None:begin=start
  text+=char;previous=end
  if char in '。！？!?\n' or len(text)>=max_chars:
   if text.strip():cues.append((begin,end,text.strip()))
   text='';begin=None
 if text.strip():cues.append((begin,previous,text.strip()))
 return cues

def timestamp(seconds):
 ms=round(seconds*1000);hours,ms=divmod(ms,3600000);minutes,ms=divmod(ms,60000);seconds,ms=divmod(ms,1000)
 return f'{hours:02}:{minutes:02}:{seconds:02},{ms:03}'

def save_response(response,directory):
 directory=Path(directory);directory.mkdir(parents=True,exist_ok=True)
 encoded=response.get('audio_base64')
 if not encoded:raise ValueError('The response has no audio_base64.')
 (directory/'audio.mp3').write_bytes(base64.b64decode(encoded,validate=True))
 cues=subtitle_cues(response)
 if not cues:raise ValueError('No non-empty subtitle cues were returned.')
 srt='\n\n'.join(f'{i}\n{timestamp(a)} --> {timestamp(b)}\n{text}' for i,(a,b,text) in enumerate(cues,1))+'\n'
 (directory/'captions.srt').write_text(srt,encoding='utf-8')
 return len(cues)

def main():
 parser=argparse.ArgumentParser(description=__doc__)
 parser.add_argument('script',nargs='?',help='UTF-8 text file')
 parser.add_argument('--out',default='output')
 parser.add_argument('--from-json',help='Saved provider response; offline conversion only')
 args=parser.parse_args()
 try:
  if args.from_json:response=json.loads(Path(args.from_json).read_text(encoding='utf-8'))
  else:
   key=os.environ.get('ELEVENLABS_API_KEY');voice=os.environ.get('VOICE_ID')
   if not key or not voice:raise ValueError('Set ELEVENLABS_API_KEY and VOICE_ID in the environment.')
   if not args.script:raise ValueError('Provide a UTF-8 script file.')
   text=Path(args.script).read_text(encoding='utf-8')
   if not text.strip():raise ValueError('The script is empty.')
   payload={'text':text,'model_id':os.environ.get('MODEL_ID','eleven_multilingual_v2')}
   url='https://api.elevenlabs.io/v1/text-to-speech/'+quote(voice,safe='')+'/with-timestamps?output_format=mp3_44100_128'
   request=Request(url,data=json.dumps(payload).encode('utf-8'),headers={'xi-api-key':key,'Content-Type':'application/json'},method='POST')
   with urlopen(request,timeout=120) as result:response=json.load(result)
  count=save_response(response,args.out)
  print(f'Saved audio.mp3 and captions.srt ({count} cues) in {args.out}')
 except HTTPError as exc:
  print(f'Provider HTTP {exc.code}. Check credentials, quota, voice and model. No retry was performed.',file=sys.stderr);return 1
 except (URLError,ValueError,KeyError,OSError,json.JSONDecodeError) as exc:
  print(str(exc),file=sys.stderr);return 1
 return 0

if __name__=='__main__':raise SystemExit(main())
