#!/usr/bin/env python3 """ Regeneriert alle MP3s, deren Text sich gegenueber audio-texts.json.bak-pre-christo-kill geaendert hat ODER deren Text einen Aussprache-Map-Key enthaelt (= braucht neue Aussprache). Logik: 1) Diff zwischen current und .bak-pre-christo-kill -> 'changed' 2) Eintraege mit Aussprache-Map-Treffern ('Montafon', 'Brandner', ...) -> 'pron' 3) Vereinigung -> regen-Liste Voice-Routing wie regenerate-mission-audios.py. Cache (audio-durations.json) wird fuer regenerierte Eintraege geloescht. Usage: python regenerate-changed-audios.py --dry # zeigt nur Liste python regenerate-changed-audios.py # regeneriert wirklich """ import json, os, sys, subprocess, tempfile, shutil, time KEY = os.environ.get('ELEVENLABS_API_KEY') or 'sk_8e21b8c2723d95fc581953c1d0fff9af3e1298faa378a651' REPO = 'C:/xampp/htdocs/geograsim/App/sims/heli' OUT_DIR = REPO + '/sounds/radio' TEXTS_FILE = REPO + '/scripts/audio-texts.json' BAK = TEXTS_FILE + '.bak-pre-christo-kill' MAP_FILE = REPO + '/data/aussprache-map.json' DUR_FILE = REPO + '/data/audio-durations.json' VOICE_SARAH = 'EXAVITQu4vr4xnSDxMaL' VOICE_BELLA = 'hpp4J3VqNfWAUOO0d1Us' def voice_for(aid): if aid.startswith('r_mission_intro_') or aid.startswith('r_mission_start_') or aid.startswith('r_tower_'): return VOICE_SARAH, 'Sarah' return VOICE_BELLA, 'Bella' def apply_pron(text, mapping): for k in sorted(mapping.keys(), key=len, reverse=True): text = text.replace(k, mapping[k]) return text def generate(aid, text, voice_id): out = OUT_DIR + '/' + aid + '.mp3' if os.path.exists(out): shutil.copy(out, out + '.bak-pre-christo-kill') body = json.dumps({ 'text': text, 'model_id': 'eleven_multilingual_v2', 'voice_settings': {'stability': 0.55, 'similarity_boost': 0.75, 'style': 0.1, 'use_speaker_boost': True} }, ensure_ascii=False) with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', suffix='.json', delete=False) as f: f.write(body); bpath = f.name r = subprocess.run(['curl','-sS','-w','%{http_code}','-o',out, '-X','POST','https://api.elevenlabs.io/v1/text-to-speech/' + voice_id, '-H','xi-api-key: ' + KEY, '-H','Content-Type: application/json; charset=utf-8', '--data-binary','@' + bpath], capture_output=True, text=True, timeout=120) os.unlink(bpath) if r.stdout.strip() == '200': return 'ok', os.path.getsize(out) return 'err-' + r.stdout, 0 def main(): if not os.path.exists(BAK): print('FEHLER: ' + BAK + ' nicht vorhanden.') sys.exit(2) current = json.load(open(TEXTS_FILE, 'r', encoding='utf-8')) bak = json.load(open(BAK, 'r', encoding='utf-8')) pron = json.load(open(MAP_FILE, 'r', encoding='utf-8'))['pronunciations'] durs = json.load(open(DUR_FILE, 'r', encoding='utf-8')) pron_keys = list(pron.keys()) changed, pron_only = [], [] for aid, txt in current.items(): if aid.startswith('_'): continue if not isinstance(txt, str): continue old = bak.get(aid, '') if txt != old: changed.append(aid) elif any(k in txt for k in pron_keys): pron_only.append(aid) to_regen = changed + pron_only print(f'Diff-changed: {len(changed)}') print(f'Pron-Map-only: {len(pron_only)} ({", ".join(pron_only) if pron_only else "-"})') print(f'Total Regen: {len(to_regen)}') if '--dry' in sys.argv: print('\n--dry-run, keine Audios generiert.') return print(f'\nStarte Regen ({len(to_regen)} MP3s)...') t0 = time.time() ok, fail = 0, 0 for i, aid in enumerate(to_regen, 1): text = current[aid] tts = apply_pron(text, pron) voice, label = voice_for(aid) status, size = generate(aid, tts, voice) elapsed = time.time() - t0 eta = (elapsed / i) * (len(to_regen) - i) if i > 0 else 0 if status == 'ok': print(f' [{i:3d}/{len(to_regen)}] OK {aid:42s} {size//1024:>4}K [{label}] ETA {eta:.0f}s') ok += 1 durs['durations'].pop(aid, None) else: print(f' [{i:3d}/{len(to_regen)}] ERR {aid:42s} {status}') fail += 1 # Zwischenspeicherung jedes 20. Mal if i % 20 == 0: json.dump(durs, open(DUR_FILE, 'w', encoding='utf-8'), indent=2, ensure_ascii=False) json.dump(durs, open(DUR_FILE, 'w', encoding='utf-8'), indent=2, ensure_ascii=False) print(f'\nFertig: {ok} OK, {fail} Fehler, {time.time()-t0:.0f}s gesamt') if __name__ == '__main__': main()