#!/usr/bin/env python3 """ Etappe 0: Legacy-Audios aus start.html / landing.html in den Standard ziehen. Aktionen: 1) Bestehende Motivations-Phrasen in r_tower_strike_c* + r_tower_handover_c* säubern 2) Neue Eintraege in audio-texts.json fuer LEGACY-Audio-IDs (ohne r_-Praefix) 3) Regenerieren aller betroffenen MP3s (mit r_-Praefix damit playRadio-Fallback greift) Texte: kurz, Pilot/Tower-Funk-Stil, KEIN "Christophorus", KEIN "ÖAMTC", KEINE Motivations-Phrasen wie "du kriegst das hin / du schaffst das". """ 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' MAP_FILE = REPO + '/data/aussprache-map.json' DUR_FILE = REPO + '/data/audio-durations.json' VOICE_SARAH = 'EXAVITQu4vr4xnSDxMaL' # Tower-Lotsin VOICE_BELLA = 'hpp4J3VqNfWAUOO0d1Us' # Pilotin # === 1) Motivations-Phrasen-Fixes (bestehende r_-Eintraege updaten) === MOTIVATION_FIXES = { # r_tower_strike_c* — alle 14 Eintraege ersetzen die Phrase '_strike_motivation': { 'old': ', du kriegst das hin.', 'new': ', noch zwei Versuche frei.', 'pattern_prefix': 'r_tower_strike_c', }, '_handover_motivation': { 'old': ' Alles gut, du lernst dazu.', 'new': '', 'pattern_prefix': 'r_tower_handover_c', }, } # === 2) Neue Eintraege fuer LEGACY-Audio-IDs === # Naming convention: ID -> (text, voice_label, ist_pilotin) # 'b' = Bella (Pilotin), 's' = Sarah (Tower-Lotsin) NEW_TEXTS = { # --- Pilot Calm (Wetter ok, gute Verhältnisse) --- 'r_pilot_calm1': ('Pilot: Wetterlage stabil. Sichtflug-Bedingungen, top. Standby.', 'b'), 'r_pilot_calm2': ('Pilot: Wind aus Nordwest, drei Beaufort. Kein Drama für den Kurs.', 'b'), 'r_pilot_calm3': ('Pilot: Sicht über fünfzehn Kilometer — wir sehen alles. Roger.', 'b'), # --- Pilot Hindernisse / Strecke (preventive Tipps) --- 'r_pilot_flock': ('Pilot: Vogelschwarm von West. Kurs anpassen, easy.', 'b'), 'r_pilot_fog1': ('Pilot: Advektionsnebel unten. Sicht unter fünfhundert Meter. Tief bleiben.', 'b'), 'r_pilot_fog2': ('Pilot: Referenzpunkt am Boden suchen — nicht am Horizont.', 'b'), 'r_pilot_fog3': ('Pilot: Lageregelung flach halten, nicht blind in die Wolke. Copy?', 'b'), 'r_pilot_gap': ('Pilot: Urbaner Canyon. Über Dachkante bleiben. Roger.', 'b'), 'r_pilot_gusts': ('Pilot: Lee-Turbulenz zwischen den Häusern — Anstellwinkel nachfassen.', 'b'), 'r_pilot_narrow': ('Pilot: Talenge voraus, Venturi-Effekt. Windseite halten. Over.', 'b'), 'r_pilot_time': ('Pilot: Thermik über den Hallen — Aufwinde, Achtung.', 'b'), 'r_pilot_trees': ('Pilot: Fichten bis fünfundzwanzig Meter — Puffer halten. Copy.', 'b'), 'r_pilot_wind': ('Pilot: Windwurf im Tal — Böen drehen. Vorsicht.', 'b'), # --- Warn (Pre-Warn fuer Hindernisse) --- 'r_warn_kran': ('Pilot: Kran rechts, vierzig Meter. Mindestabstand halten.', 'b'), 'r_warn_antenne': ('Pilot: Antenne links, einhundertzwanzig Meter hoch. Umfliegen mit Puffer.', 'b'), 'r_warn_strom': ('Pilot: Hochspannungsleitung voraus. Drüber, nicht drunter. Roger.', 'b'), 'r_warn_schornstein': ('Pilot: Schornsteine als Festpunkte nutzen — visuelle Referenz.', 'b'), 'r_warn_fabrik': ('Pilot: Abwindzone links. Höher und nach rechts.', 'b'), 'r_warn_nebel': ('Pilot: Nebel dichtet ein. Kurs halten — vorsichtig.', 'b'), 'r_warn_gewitter': ('Pilot: Gewitterzelle voraus, sieben Meilen. Umfliegen.', 'b'), 'r_warn_fuel': ('Pilot: Treibstoff knapp. Klinikkurs halten.', 'b'), # --- Warn (Crash-Reaction — wird nach Strike gespielt) --- 'r_warn_vogel': ('Pilot: Vogelschlag! Rotor checken.', 'b'), 'r_warn_haus': ('Pilot: Gebäude touchiert. Schaden auswerten.', 'b'), 'r_warn_baum': ('Pilot: Baum gestreift. Höhe rauf.', 'b'), 'r_warn_klima': ('Pilot: Klimaanlage gerissen. Abstand halten.', 'b'), 'r_warn_blitz': ('Pilot: Blitz nahe! Sofort raus aus der Zelle.', 'b'), 'r_warn_flugzeug': ('Pilot: Verkehrsflugzeug! Sofort tiefer.', 'b'), 'r_warn_sport': ('Pilot: Sportflieger im Kurs! Ausweichen.', 'b'), # --- Tower generic (kein Heli-Name) --- 'r_tower1': ('Tower: Pilot, Flugplan bestätigt. Position halten, Freigabe folgt.', 's'), 'r_tower2': ('Tower: Wind im Rahmen. Sektor klar, weiter im Plan.', 's'), 'r_tower3': ('Tower: Sektor frei. Anflugkorridor ist offen.', 's'), 'r_tower4': ('Tower: Frequenz wechseln. Standby auf neuer Sektor-Freigabe.', 's'), # --- Success (V2-Backup, normalerweise von V1 geblockt) --- 'r_success2': ('Tower: Sauber abgefangen. Mission steht.', 's'), } 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-etappe0') 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) return ('ok' if r.stdout.strip() == '200' else 'err-' + r.stdout), os.path.getsize(out) if os.path.exists(out) else 0 def main(): texts = json.load(open(TEXTS_FILE, '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')) # === Schritt 1: Motivations-Phrasen-Fixes === print('=== Schritt 1: Motivations-Phrasen-Fixes ===') motivation_changed = [] for fix in MOTIVATION_FIXES.values(): for aid, txt in list(texts.items()): if isinstance(txt, str) and aid.startswith(fix['pattern_prefix']): if fix['old'] in txt: texts[aid] = txt.replace(fix['old'], fix['new']) motivation_changed.append(aid) print(f' {len(motivation_changed)} Eintraege gefixt') # === Schritt 2: Neue Eintraege importieren === print('\n=== Schritt 2: Neue Eintraege ===') for aid, (txt, voice) in NEW_TEXTS.items(): texts[aid] = txt print(f' {len(NEW_TEXTS)} neue Eintraege') # Speichern with open(TEXTS_FILE, 'w', encoding='utf-8') as f: json.dump(texts, f, indent=2, ensure_ascii=False) print(f'\n -> audio-texts.json gespeichert ({len(texts)} Eintraege total)') if '--dry' in sys.argv: print('\n--dry-run, keine MP3s generiert.') return # === Schritt 3: Regen === print('\n=== Schritt 3: Regen-Lauf ===') regen_list = motivation_changed + list(NEW_TEXTS.keys()) print(f' Total: {len(regen_list)} MP3s') print() t0 = time.time() ok, fail = 0, 0 for i, aid in enumerate(regen_list, 1): text = texts[aid] tts = apply_pron(text, pron) # Voice-Routing if aid in NEW_TEXTS: voice = VOICE_BELLA if NEW_TEXTS[aid][1] == 'b' else VOICE_SARAH label = 'Bella' if NEW_TEXTS[aid][1] == 'b' else 'Sarah' elif aid.startswith('r_tower_'): voice = VOICE_SARAH; label = 'Sarah' else: voice = VOICE_BELLA; label = 'Bella' status, size = generate(aid, tts, voice) elapsed = time.time() - t0 eta = (elapsed / i) * (len(regen_list) - i) if status == 'ok': print(f' [{i:2d}/{len(regen_list)}] OK {aid:38s} {size//1024:>4}K [{label}] ETA {eta:.0f}s') ok += 1 durs['durations'].pop(aid, None) else: print(f' [{i:2d}/{len(regen_list)}] ERR {aid:38s} {status}') fail += 1 if i % 10 == 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') if __name__ == '__main__': main()