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>
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
#!/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()
|