79ac9dad84
- Logistik: Musik-Player Playlist-Modus (5 Gruppen, 20 Tracks) - Heli: End-Screen auf .ggs-endscreen, Mission-Bilder, Voice-Lines, Briefing-Audio - Logistik: Layout-Tausch Auftraege+Fahrzeuge links, Karte rechts - Logistik: Tier-1-Staedte ausgebaut, Auto-Timescale-Badge - Logistik: Roadnet/Railnet Dijkstra-Routing, Balance-Updates - Atlas: 5 Atlas-Inbox-Nachrichten (Cards-Pattern, DALL-E-Key, Musik-Playlists) - Status-Updates Heli + Logistik Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.1 KiB
Python
103 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generiert alle Voice-Line-MP3s via OpenAI TTS API.
|
|
|
|
Liest App/sims/logistik/assets/audio/voice-lines.json und legt pro Eintrag
|
|
zwei Dateien an: <id>.mp3 (std) und <id>_easy.mp3 (easy).
|
|
|
|
Aufruf:
|
|
set -a; source App/.env.local; set +a
|
|
python App/sims/logistik/scripts/generate-voice-lines.py
|
|
|
|
Vorhandene Dateien werden uebersprungen (FORCE=1 zum Ueberschreiben).
|
|
"""
|
|
import json
|
|
import os
|
|
import ssl
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
LINES_JSON = SCRIPT_DIR.parent / "assets" / "audio" / "voice-lines.json"
|
|
OUT_DIR = SCRIPT_DIR.parent / "assets" / "audio" / "voice" / "de"
|
|
|
|
API_KEY = os.environ.get("OPENAI_API_KEY")
|
|
if not API_KEY:
|
|
print("FEHLER: OPENAI_API_KEY muss als Env-Variable gesetzt sein.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
FORCE = bool(os.environ.get("FORCE"))
|
|
|
|
SSL_CTX = ssl.create_default_context()
|
|
SSL_CTX.check_hostname = False
|
|
SSL_CTX.verify_mode = ssl.CERT_NONE
|
|
|
|
|
|
def tts(text, voice="nova", model="tts-1", speed=1.0):
|
|
"""Ruft OpenAI TTS auf, gibt mp3-bytes zurueck."""
|
|
payload = json.dumps({
|
|
"model": model,
|
|
"voice": voice,
|
|
"input": text,
|
|
"speed": speed,
|
|
"response_format": "mp3",
|
|
}).encode("utf-8")
|
|
req = urllib.request.Request(
|
|
"https://api.openai.com/v1/audio/speech",
|
|
data=payload,
|
|
headers={
|
|
"Authorization": "Bearer " + API_KEY,
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=60, context=SSL_CTX) as resp:
|
|
return resp.read()
|
|
|
|
|
|
def main():
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
data = json.loads(LINES_JSON.read_text(encoding="utf-8"))
|
|
voice = data.get("_voice", "nova")
|
|
model = data.get("_model", "tts-1")
|
|
lines = data["lines"]
|
|
|
|
count_new = 0
|
|
count_skip = 0
|
|
total_chars = 0
|
|
|
|
for entry in lines:
|
|
lid = entry["id"]
|
|
for variant, text_key, speed in (("", "std", 1.0), ("_easy", "easy", 0.88)):
|
|
text = entry.get(text_key)
|
|
if not text:
|
|
continue
|
|
fname = f"{lid}{variant}.mp3"
|
|
out_path = OUT_DIR / fname
|
|
if out_path.exists() and not FORCE:
|
|
count_skip += 1
|
|
print(f"ok {fname} (schon da)")
|
|
continue
|
|
total_chars += len(text)
|
|
print(f"-> {fname}: {len(text)} Zeichen, speed={speed} ...", end="", flush=True)
|
|
try:
|
|
mp3 = tts(text, voice=voice, model=model, speed=speed)
|
|
except Exception as e:
|
|
print(f" FEHLER: {e}")
|
|
continue
|
|
out_path.write_bytes(mp3)
|
|
size_kb = len(mp3) / 1024
|
|
print(f" {size_kb:.0f} KB")
|
|
count_new += 1
|
|
time.sleep(0.35) # sanftes Rate-Limit
|
|
|
|
# tts-1 Kosten: $15 / 1M Zeichen (2024)
|
|
cost_eur = total_chars / 1_000_000 * 15 * 0.92 # USD -> EUR approx
|
|
print(f"\nFertig: {count_new} neu, {count_skip} uebersprungen.")
|
|
print(f"Zeichen neu erzeugt: {total_chars} (~{cost_eur:.3f} EUR)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|