"""ハコブくんの声（ElevenLabs）で、台本を1文ずつ音声にする。エイイチの Mac で動かす（API キーを Claude に渡さないため）。

準備（1回だけ）
  1. ElevenLabs の有料プランに入る（無料プランの音声は商用に使えない）
  2. Voice Design で声を作って保存し、その声の Voice ID を控える（Voices の一覧で声を開くと出る）
  3. ターミナルで API キーを入れる（キーは画面やファイルに残さない）:  export ELEVENLABS_API_KEY=（キー）

使い方（動画の作業フォルダで。例: 動画/AIエージェントの取り違え/）
  python3 ../_制作ベース/tts_eleven.py --voice VOICE_ID --name 候補A --lines 0-12
    --lines   作る文の番号（0 から。「0-12」「5,8,9」のように書く。省略するとすべて）
    --model   eleven_v4（既定。2026-10-12 に決定）／eleven_multilingual_v2／eleven_v3
    --seed    同じ値なら、なるべく同じ読み方になる（既定 7）
    --try     台本ではなく、試しの文を作る（「｜」で区切って何文でも。例: --try "エイイチは…。｜栄一は…。"）
              出力は voice_eleven/{名前}/試し_01.mp3 … と、どの文かの一覧 試し.txt
出力: voice_eleven/{名前}/{番号}.mp3 と、作った設定の記録 voice_eleven/{名前}/設定.json
このあと Claude が tts_import.py で 48kHz の wav にして、口の動きと字幕に使う。
使う文字数（クレジット）は、文の字数の合計。1本約4,100字。作り直した分も数える
"""
import argparse, json, os, sys, time, urllib.request, urllib.error
sys.path.insert(0, '.')
from narration import SEGS
try:
    from narration import parse
except ImportError:
    def parse(s):
        return s[0], "base", s[1], (s[2] if len(s) > 2 else 0.35)
try:
    from narration import ELEVEN_READ   # ElevenLabs が読み間違えた語だけ、ここで読みを直す（字幕は変えない）
except ImportError:
    ELEVEN_READ = []

ap = argparse.ArgumentParser()
ap.add_argument('--voice', required=True)
ap.add_argument('--name', required=True)
ap.add_argument('--lines', default='')
ap.add_argument('--model', default='eleven_v4')   # 2026-10-12 エイイチが v4 に決めた（冒頭14文で v2 と聞き比べ）
ap.add_argument('--seed', type=int, default=7)
ap.add_argument('--stability', type=float, default=0.55)
ap.add_argument('--similarity', type=float, default=0.75)
ap.add_argument('--try', dest='trial', default='')
args = ap.parse_args()
key = os.environ.get('ELEVENLABS_API_KEY')
if not key:
    sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで export ELEVENLABS_API_KEY=（キー） を実行してから動かしてください')


def nums(spec, n):
    if not spec:
        return list(range(n))
    out = []
    for part in spec.split(','):
        if '-' in part:
            a, b = part.split('-'); out += list(range(int(a), int(b) + 1))
        else:
            out.append(int(part))
    return [i for i in out if 0 <= i < n]


def speak_text(t):
    t = t.replace('｜', '').replace('「', '').replace('」', '')
    for a, b in ELEVEN_READ:
        t = t.replace(a, b)
    return t


texts = [parse(s)[2] for s in SEGS]
outdir = os.path.join('voice_eleven', args.name)
if args.trial:   # 試しの文だけを作る（台本の読みの直し ELEVEN_READ は使わない。書いたとおりに読ませて聞き比べる）
    os.makedirs(outdir, exist_ok=True)
    lines = [t for t in args.trial.split('｜') if t]
    with open(os.path.join(outdir, '試し.txt'), 'w', encoding='utf-8') as f:
        for k, t in enumerate(lines, 1):
            f.write(f'試し_{k:02d}: {t}\n')
    for k, t in enumerate(lines, 1):
        body = {'text': t, 'model_id': args.model, 'seed': args.seed,
                'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}
        req = urllib.request.Request(
            f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',
            data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})
        try:
            with urllib.request.urlopen(req, timeout=120) as r:
                open(os.path.join(outdir, f'試し_{k:02d}.mp3'), 'wb').write(r.read())
        except urllib.error.HTTPError as e:
            sys.exit(f'試し_{k:02d} で止まりました: {e.code} {e.read().decode(errors="ignore")[:300]}')
        print(f'試し_{k:02d} {t}')
    print(f'できあがり: {outdir}  使った文字数 約{sum(len(t) for t in lines)}字')
    sys.exit(0)
os.makedirs(outdir, exist_ok=True)
json.dump(dict(vars(args), time=time.strftime('%Y-%m-%d %H:%M')), open(os.path.join(outdir, '設定.json'), 'w'),
          ensure_ascii=False, indent=1)
used = 0
for i in nums(args.lines, len(SEGS)):
    if not texts[i]:
        continue
    prev = next((speak_text(texts[j]) for j in range(i - 1, -1, -1) if texts[j]), None)
    nxt = next((speak_text(texts[j]) for j in range(i + 1, len(texts)) if texts[j]), None)
    body = {'text': speak_text(texts[i]), 'model_id': args.model, 'seed': args.seed,
            'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}
    if args.model.startswith('eleven_multilingual'):   # 前後の文を渡すと、文のつながりの抑揚が自然になる（対応を確かめたモデルだけ）
        body['previous_text'] = prev
        body['next_text'] = nxt
    req = urllib.request.Request(
        f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',
        data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            open(os.path.join(outdir, f'{i:03d}.mp3'), 'wb').write(r.read())
    except urllib.error.HTTPError as e:
        sys.exit(f'{i:03d} で止まりました: {e.code} {e.read().decode(errors="ignore")[:300]}')
    except urllib.error.URLError as e:
        sys.exit(f'{i:03d} で止まりました（ElevenLabs につながらない）: {e.reason}')
    used += len(body['text'])
    print(f'{i:03d} {body["text"][:30]}')
print(f'できあがり: {outdir}  使った文字数 約{used}字')
