1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
181 lines
8.6 KiB
Python
181 lines
8.6 KiB
Python
#!/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()
|