#!/usr/bin/env python3
"""
generate_audio.py — 用 OpenAI TTS 把 english_units.html 裡所有英文句子跑成 mp3。

用法：
    export OPENAI_API_KEY=你的key
    python3 generate_audio.py [--force] [--voice shimmer] [--speed 0.9]

不需要任何套件（純 Python 標準函式庫），不需要 ffmpeg（OpenAI 直接回 mp3）。

聲音：alloy / ash / ballad / coral / echo / fable / nova / onyx / sage / shimmer / verse
模型：gpt-4o-mini-tts（預設，支援 instructions）/ tts-1-hd / tts-1
"""
import argparse
import hashlib
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent
HTML = ROOT / "english_units.html"
AUDIO_DIR = ROOT / "audio"
OPENAI_TTS_URL = "https://api.openai.com/v1/audio/speech"

INSTRUCTIONS = (
    "Speak in a warm, playful, encouraging tone like a cheerful older sister reading a "
    "picture book to a young child. Clear American English, slightly slower than normal, "
    "full of warmth and excitement. Sound friendly and approachable, not stiff. "
    "Pronounce every word clearly."
)


def extract_texts(html_text: str):
    pattern = re.compile(r'en:\s*"((?:[^"\\]|\\.)*)"')
    clean, seen = [], set()
    for m in pattern.findall(html_text):
        t = m.replace('\\"', '"').replace("\\'", "'")
        t = re.sub(r'<[^>]+>', '', t).strip()
        if t and t not in seen:
            seen.add(t)
            clean.append(t)
    return clean


def hash_text(text: str) -> str:
    return hashlib.sha1(text.encode('utf-8')).hexdigest()[:16]


def tts(api_key: str, text: str, voice: str, model: str,
        fmt: str = 'mp3', speed: float = 0.9) -> bytes:
    payload = {
        "model": model,
        "input": text,
        "voice": voice,
        "response_format": fmt,
        "speed": speed,
    }
    if model.startswith("gpt-4o"):
        payload["instructions"] = INSTRUCTIONS
    req = urllib.request.Request(
        OPENAI_TTS_URL,
        data=json.dumps(payload).encode('utf-8'),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
    )
    with urllib.request.urlopen(req, timeout=90) as r:
        return r.read()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--force', action='store_true', help='重新產生已存在的檔案')
    ap.add_argument('--voice', default='nova',
                    help='alloy/ash/ballad/coral/echo/fable/nova/onyx/sage/shimmer/verse')
    ap.add_argument('--speed', type=float, default=0.92,
                    help='0.25–4.0，<1 變慢（預設 0.92）')
    ap.add_argument('--model', default='gpt-4o-mini-tts',
                    choices=['gpt-4o-mini-tts', 'tts-1-hd', 'tts-1'])
    ap.add_argument('--format', default='mp3',
                    choices=['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm'])
    ap.add_argument('--sleep', type=float, default=0.2)
    args = ap.parse_args()

    api_key = os.getenv('OPENAI_API_KEY')
    if not api_key:
        print('需要 OPENAI_API_KEY', file=sys.stderr)
        print('  去 https://platform.openai.com/api-keys 拿', file=sys.stderr)
        sys.exit(1)

    AUDIO_DIR.mkdir(exist_ok=True)
    texts = extract_texts(HTML.read_text(encoding='utf-8'))
    print(f'找到 {len(texts)} 句不重複英文')
    print(f'模型 {args.model}　聲音 {args.voice}　速度 {args.speed}x　格式 {args.format}')
    print()

    manifest = {}
    made = skipped = failed = 0

    for i, text in enumerate(texts, 1):
        h = hash_text(text)
        out = AUDIO_DIR / f'{h}.{args.format}'
        label = text if len(text) <= 55 else text[:52] + '...'

        if out.exists() and not args.force:
            manifest[text] = out.name
            skipped += 1
            print(f'[{i:3}/{len(texts)}] skip   {label}')
            continue

        print(f'[{i:3}/{len(texts)}] gen    {label}')
        try:
            audio = tts(api_key, text, args.voice, args.model, args.format, args.speed)
            out.write_bytes(audio)
            manifest[text] = out.name
            made += 1
        except urllib.error.HTTPError as e:
            failed += 1
            body = e.read().decode('utf-8', errors='ignore')[:240]
            print(f'          HTTP {e.code}: {body}')
        except Exception as e:
            failed += 1
            print(f'          FAIL: {e}')

        time.sleep(args.sleep)

    (AUDIO_DIR / 'manifest.json').write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')
    (AUDIO_DIR / 'manifest.js').write_text(
        'window.__audioManifest = ' + json.dumps(manifest, ensure_ascii=False) + ';\n',
        encoding='utf-8',
    )

    print()
    print(f'完成：新產生 {made}、跳過 {skipped}、失敗 {failed}')
    print(f'檔案在 {AUDIO_DIR}/')


if __name__ == '__main__':
    main()
