#!/usr/bin/env python3 """ Regeneriert die Audios fuer eine oder mehrere Heli-Missionen via ElevenLabs. Welche Audios pro Mission: r_mission_intro_ (Sarah/Tower) r_mission_geo_ (Bella/Briefing) r_mission_start_ (Sarah/Tower) r_tower_geo_ (Sarah/Tower) r_wp_geo_ (Bella/Briefing) fuer jeden Wegpunkt der Mission Aussprache-Map wird auf TTS-Input angewendet (data/aussprache-map.json). Cache-Eintraege in audio-durations.json werden geloescht (neu gemessen beim naechsten Tool-Load). Usage: python regenerate-mission-audios.py m2 python regenerate-mission-audios.py m2 m3 m4 python regenerate-mission-audios.py all python regenerate-mission-audios.py m2 --skip-existing # nur fehlende """ import json, os, sys, subprocess, tempfile, shutil 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' MISSIONS_FILE = REPO + '/data/missions.json' MAP_FILE = REPO + '/data/aussprache-map.json' DUR_FILE = REPO + '/data/audio-durations.json' VOICE_SARAH = 'EXAVITQu4vr4xnSDxMaL' # Tower-Lotsin VOICE_BELLA = 'hpp4J3VqNfWAUOO0d1Us' # Pilotin/Briefing 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_pronunciation(text, mapping): # Laengere Keys zuerst, damit 'Brandnertal' vor 'Brandner' greift for k in sorted(mapping.keys(), key=len, reverse=True): text = text.replace(k, mapping[k]) return text def generate_one(aid, text, voice_id, skip_existing=False): out = OUT_DIR + '/' + aid + '.mp3' if skip_existing and os.path.exists(out): return 'skip-existing', 0 if os.path.exists(out): shutil.copy(out, out + '.bak-pre-regen') 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=90) os.unlink(bpath) if r.stdout.strip() == '200': return 'ok', os.path.getsize(out) return 'err-http-' + r.stdout, 0 def audios_for_mission(mission, texts): """Liefert Liste der Audio-IDs, die fuer eine Mission existieren sollen.""" mid = mission['id'] wps = mission.get('waypoints', []) ids = [ 'r_mission_intro_' + mid, 'r_mission_geo_' + mid, 'r_mission_start_' + mid, 'r_tower_geo_' + mid, ] for w in wps: ids.append('r_wp_geo_' + w) # Filtere existierende return [aid for aid in ids if aid in texts] def main(): args = sys.argv[1:] if not args: print(__doc__) sys.exit(1) skip_existing = '--skip-existing' in args args = [a for a in args if not a.startswith('--')] texts = json.load(open(TEXTS_FILE, 'r', encoding='utf-8')) missions = json.load(open(MISSIONS_FILE, 'r', encoding='utf-8'))['missions'] pron_map = json.load(open(MAP_FILE, 'r', encoding='utf-8'))['pronunciations'] if 'all' in args: targets = missions else: targets = [m for m in missions if m['id'] in args] if not targets: print('Keine Mission gefunden fuer: ' + ', '.join(args)) sys.exit(2) durj = json.load(open(DUR_FILE, 'r', encoding='utf-8')) total_ok = 0; total_fail = 0; total_skip = 0 for mission in targets: mid = mission['id'] aids = audios_for_mission(mission, texts) print(f'\n=== {mid} ({mission.get("title","?")}) — {len(aids)} Audios ===') for aid in aids: text = texts[aid] tts_text = apply_pronunciation(text, pron_map) voice, label = voice_for(aid) status, size = generate_one(aid, tts_text, voice, skip_existing) if status == 'ok': print(f' OK {aid:36s} {size//1024:>5}K [{label}]') total_ok += 1 durj['durations'].pop(aid, None) # cache leeren elif status == 'skip-existing': print(f' -- {aid:36s} skip (existiert schon)') total_skip += 1 else: print(f' ERR {aid:36s} {status}') total_fail += 1 json.dump(durj, open(DUR_FILE, 'w', encoding='utf-8'), indent=2, ensure_ascii=False) print(f'\nGesamt: {total_ok} generiert, {total_skip} uebersprungen, {total_fail} Fehler') if __name__ == '__main__': main()