.json und * erzeugt MP3s in App/sims/heli/sounds/radio/.mp3 * * Aufruf via Browser: * http://localhost/geograsim/App/php/generate-audio-openai.php?file=tower-geo-lines.json * http://localhost/geograsim/App/php/generate-audio-openai.php?file=tower-start-lines.json&voice=shimmer * ?force=1 -> ueberschreibt bestehende */ set_time_limit(0); ignore_user_abort(true); header('Content-Type: text/plain; charset=utf-8'); $envFile = __DIR__ . '/../.env.local'; if (!file_exists($envFile)) { exit("ERROR: .env.local fehlt\n"); } $OPENAI_KEY = ''; foreach (file($envFile) as $ln) { $ln = trim($ln); if ($ln === '' || $ln[0] === '#') continue; if (preg_match('/^OPENAI_API_KEY\s*=\s*(.+)$/', $ln, $m)) { $OPENAI_KEY = trim($m[1], " \t\"'"); break; } } if (!$OPENAI_KEY) { exit("ERROR: OPENAI_API_KEY nicht in .env.local gefunden\n"); } $file = $_GET['file'] ?? 'tower-geo-lines.json'; $voice = $_GET['voice'] ?? 'nova'; $model = $_GET['model'] ?? 'gpt-4o-mini-tts'; $force = !empty($_GET['force']); $instr = "Speak warmly and patiently like an encouraging female flight controller guiding a young student co-pilot. Unhurried, clear, friendly German pronunciation. Gentle reassuring tone, never robotic."; $jsonPath = __DIR__ . '/../sims/heli/scripts/' . basename($file); if (!file_exists($jsonPath)) { exit("ERROR: JSON nicht gefunden: $jsonPath\n"); } $data = json_decode(file_get_contents($jsonPath), true); if (!$data || !isset($data['lines'])) { exit("ERROR: JSON ungültig\n"); } $outDir = __DIR__ . '/../sims/heli/sounds/radio'; if (!is_dir($outDir)) mkdir($outDir, 0777, true); echo "Modell: $model · Stimme: $voice\n"; echo "Quelle: $file\n"; echo "Zeilen: " . count($data['lines']) . "\n"; echo str_repeat('-', 60) . "\n"; @ob_flush(); @flush(); $ok = 0; $skip = 0; $fail = 0; foreach ($data['lines'] as $line) { $id = $line['id'] ?? null; $text = $line['text'] ?? null; if (!$id || !$text) continue; $out = $outDir . '/' . $id . '.mp3'; if (file_exists($out) && !$force) { $skip++; echo "skip $id.mp3 (vorhanden)\n"; @ob_flush(); @flush(); continue; } $body = [ 'model' => $model, 'voice' => $voice, 'input' => $text, 'response_format' => 'mp3', 'speed' => isset($_GET['speed']) ? floatval($_GET['speed']) : 1.12, ]; if (strpos($model, 'mini-tts') !== false) { $body['instructions'] = $instr; } $ch = curl_init('https://api.openai.com/v1/audio/speech'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $OPENAI_KEY, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, ]); $resp = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($http !== 200 || strlen($resp) < 1500) { $fail++; $snippet = substr($resp ?: $err, 0, 200); echo "FAIL $id -> HTTP $http · $snippet\n"; @ob_flush(); @flush(); // bei Quota/Key-Problem abbrechen if ($http === 401 || $http === 429 || $http === 403) { echo "STOP (Auth- oder Quota-Problem)\n"; break; } continue; } file_put_contents($out, $resp); $ok++; echo "ok $id.mp3 (" . strlen($resp) . " Bytes)\n"; @ob_flush(); @flush(); usleep(200000); // 0.2 s } echo str_repeat('-', 60) . "\n"; echo "Zusammenfassung: ok=$ok, skip=$skip, fail=$fail\n"; $totalMp3 = count(glob($outDir . '/*.mp3')); echo "Dateien im Ordner insgesamt: $totalMp3\n";