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>
130 lines
5.0 KiB
Python
130 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Regeneriert die Audios fuer eine oder mehrere Heli-Missionen via ElevenLabs.
|
|
|
|
Welche Audios pro Mission:
|
|
r_mission_intro_<mid> (Sarah/Tower)
|
|
r_mission_geo_<mid> (Bella/Briefing)
|
|
r_mission_start_<mid> (Sarah/Tower)
|
|
r_tower_geo_<mid> (Sarah/Tower)
|
|
r_wp_geo_<wpkey> (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()
|