Files
Adminator dfb9e80664 Klima: Engine extrahiert + 2D refactored + Drama-Track + Bugfixes
- engine.js als headless Single Source of Truth (KlimaEngine)
- game-2d.html nutzt engine (-515 Zeilen Duplikation)
- rising-pressure.mp3 als Drama-Slot, Auto-Switch bei kritischem State
- state.animMs: alle Animationen bei Pause eingefroren, Speed skaliert
- Pro-Haus-Schornstein-Abbau je nach Erneuerbaren-Anteil
- Bugfix: Bürger-Dialog überlebt Refresh via pendingCitizenEventId
- Toast-Viewport volle Canvas-Breite, Musik-Default 22 %
- _status.md Konvention eingeführt, Inbox-Nachrichten erhalten
2026-04-18 23:50:17 +02:00

126 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Klimawächter SFX-Generator für ElevenLabs Sound Effects API.
Liest App/.env.local (ELEVENLABS_API_KEY) + sounds-list.json und lädt für
jeden Eintrag einen MP3 von ElevenLabs in ../assets/sounds/. Existierende
Dateien werden übersprungen (für inkrementelles Laufenlassen).
Aufruf:
cd App/sims/klima/scripts
python generate-sounds.py # alle fehlenden Sounds generieren
python generate-sounds.py --force # ALLE neu generieren (überschreibt)
python generate-sounds.py ui-click # nur diesen einen Sound
"""
import json
import os
import sys
import time
import urllib.request
import urllib.error
from pathlib import Path
API_URL = "https://api.elevenlabs.io/v1/sound-generation"
SCRIPT_DIR = Path(__file__).resolve().parent
SOUNDS_LIST = SCRIPT_DIR / "sounds-list.json"
OUT_DIR = SCRIPT_DIR.parent / "assets" / "sounds"
APP_ROOT = SCRIPT_DIR.parent.parent.parent # App/
ENV_FILE = APP_ROOT / ".env.local"
def load_env(file_path: Path) -> dict:
env = {}
if not file_path.exists():
return env
for line in file_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
env[key.strip()] = val.strip()
return env
def generate_sound(api_key: str, prompt: str, duration: float, influence: float) -> bytes:
body = json.dumps({
"text": prompt,
"duration_seconds": duration,
"prompt_influence": influence,
}).encode("utf-8")
req = urllib.request.Request(
API_URL,
data=body,
method="POST",
headers={
"xi-api-key": api_key,
"Content-Type": "application/json",
"Accept": "audio/mpeg",
},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return resp.read()
def main() -> int:
env = load_env(ENV_FILE)
api_key = env.get("ELEVENLABS_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
if not api_key:
print(f"FEHLER: ELEVENLABS_API_KEY nicht in {ENV_FILE} oder Umgebung gefunden.", file=sys.stderr)
return 1
force = "--force" in sys.argv
only_file = None
for arg in sys.argv[1:]:
if arg.startswith("--"):
continue
only_file = arg + ".mp3" if not arg.endswith(".mp3") else arg
break
data = json.loads(SOUNDS_LIST.read_text(encoding="utf-8"))
sounds = data["sounds"]
OUT_DIR.mkdir(parents=True, exist_ok=True)
total = len(sounds)
generated = 0
skipped = 0
errors = 0
for i, s in enumerate(sounds, 1):
name = s["file"]
out = OUT_DIR / name
if only_file and name != only_file:
continue
if out.exists() and not force:
skipped += 1
print(f"[{i:2}/{total}] SKIP {name:28} (schon da, --force überschreibt)")
continue
prompt = s["prompt"]
dur = float(s.get("duration", 2.0))
infl = float(s.get("influence", 0.5))
print(f"[{i:2}/{total}] GEN {name:28} dur={dur}s → {prompt[:60]}…")
try:
audio = generate_sound(api_key, prompt, dur, infl)
out.write_bytes(audio)
generated += 1
# ElevenLabs verlangt keine Rate-Limit-Pause, aber 0,5s Pause
# reduziert Last und macht Fehler besser lesbar.
time.sleep(0.5)
except urllib.error.HTTPError as e:
msg = e.read().decode("utf-8", errors="ignore")[:200]
print(f" HTTP {e.code}: {msg}", file=sys.stderr)
errors += 1
except Exception as e:
print(f" Fehler: {e}", file=sys.stderr)
errors += 1
print()
print(f"Ergebnis: {generated} neu, {skipped} übersprungen, {errors} Fehler")
print(f"Zielordner: {OUT_DIR}")
return 0 if errors == 0 else 1
if __name__ == "__main__":
sys.exit(main())