#!/usr/bin/env python3 """ GeoGraSim — Zentraler SFX-Generator für ElevenLabs Sound Effects API. Generiert MP3-Dateien aus einem sounds-list.json pro Modul. Existierende Dateien werden standardmäßig übersprungen. Die Pipeline ist identisch für alle Module — jedes Modul hat nur seine eigene sounds-list.json mit passenden Prompts. Struktur (pro Modul): App/sims//scripts/sounds-list.json (Definitionen) App/sims//assets/sounds/*.mp3 (Output) Key: App/.env.local → ELEVENLABS_API_KEY=... (in .gitignore) Aufruf: python App/scripts/generate-sounds.py # fehlende Sounds python App/scripts/generate-sounds.py --force # alle neu python App/scripts/generate-sounds.py ui-click # nur einen Beispiele: python App/scripts/generate-sounds.py klima python App/scripts/generate-sounds.py heli --force python App/scripts/generate-sounds.py fluss build-dam """ 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 APP_ROOT = SCRIPT_DIR.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 usage(): print(__doc__) sys.exit(1) def main() -> int: if len(sys.argv) < 2 or sys.argv[1].startswith("--"): usage() module = sys.argv[1] sounds_list = APP_ROOT / "sims" / module / "scripts" / "sounds-list.json" out_dir = APP_ROOT / "sims" / module / "assets" / "sounds" if not sounds_list.exists(): print(f"FEHLER: {sounds_list} nicht gefunden.", file=sys.stderr) print(f"Lege für das Modul '{module}' zuerst eine sounds-list.json an.", file=sys.stderr) return 1 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[2:]: 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 print(f"=== Modul: {module} ({total} Sounds) ===") 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:32} (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:32} dur={dur}s → {prompt[:54]}…") try: audio = generate_sound(api_key, prompt, dur, infl) out.write_bytes(audio) generated += 1 time.sleep(0.5) # Sanfte Pause zwischen Requests 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 # 401 = Kein Credit → sofort abbrechen, Thomas muss aufladen if e.code == 401: print(" → API-Key invalid oder Budget aufgebraucht. Abbruch.", file=sys.stderr) return 2 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())