Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit
- Design-System (assets/css/design-system.css) mit 21 Komponenten, iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player, Glossar-Tooltips - Templates (sims/template.html + student/teacher-Dashboard) - Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache, 4c iPad), content-architecture.md, crash-recovery.md, music-registry.md - Admin-Infrastruktur: admin-modules.html + api/admin-modules.php (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul) - Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings an Klima, Glossar, Lehrplan, Fluss - Zentrale SFX-Pipeline (scripts/generate-sounds.py) - DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat) - Logo + Inter-Font lokal - PHP-APIs: admin, glossar, levels, licenses, progress, waypoints, assignments, profile, tickets - Spielsprache entfernt (admin-modules, admin-levels, schueler) - Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#!/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/<modul>/scripts/sounds-list.json (Definitionen)
|
||||
App/sims/<modul>/assets/sounds/*.mp3 (Output)
|
||||
|
||||
Key:
|
||||
App/.env.local → ELEVENLABS_API_KEY=... (in .gitignore)
|
||||
|
||||
Aufruf:
|
||||
python App/scripts/generate-sounds.py <modul> # fehlende Sounds
|
||||
python App/scripts/generate-sounds.py <modul> --force # alle neu
|
||||
python App/scripts/generate-sounds.py <modul> 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())
|
||||
Reference in New Issue
Block a user