.mp3. Idempotent — überspringt * existierende Dateien. * * Aufruf: php scripts/generate-sounds.php [--force] [--only=name1,name2] */ $ENV_FILE = __DIR__ . '/../../../.env.local'; if (!file_exists($ENV_FILE)) { fwrite(STDERR, "Missing $ENV_FILE\n"); exit(1); } // Manuelles Parsen — parse_ini_file scheitert an dt. Sonderzeichen in Kommentaren. $KEY = null; foreach (file($ENV_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') continue; if (strpos($line, '=') === false) continue; [$k, $v] = explode('=', $line, 2); if (trim($k) === 'ELEVENLABS_API_KEY') { $KEY = trim($v, " \"'"); break; } } if (!$KEY) { fwrite(STDERR, "ELEVENLABS_API_KEY nicht in .env.local\n"); exit(1); } $OUT_DIR = __DIR__ . '/../assets/sounds/sfx'; if (!is_dir($OUT_DIR)) mkdir($OUT_DIR, 0755, true); $args = array_slice($argv, 1); $force = in_array('--force', $args); $only = null; foreach ($args as $a) { if (strpos($a, '--only=') === 0) { $only = explode(',', substr($a, 7)); } } $SOUNDS = [ // Aktions-Sounds (15) 'harvest_start' => ['Distant farm bell ringing twice, calling workers to harvest, warm ambient countryside', 1.2], 'report_open' => ['Soft paper page turning, gentle crisp sound', 0.5], 'report_next' => ['Soft single tap on wooden table, very brief', 0.5], 'vehicle_truck' => ['Heavy diesel truck engine starting and driving away, departing motor', 1.5], 'vehicle_ship' => ['Cargo ship horn, deep low blast, single foghorn at sea', 2.0], 'vehicle_plane' => ['Cargo plane taking off, jet engine spinning up and lifting', 2.0], 'vehicle_arrive' => ['Cash register cha-ching sound, coins falling into drawer, brief and bright', 0.8], 'disease_outbreak' => ['Ominous low drone with subtle crackling and whispering wind, foreboding agricultural disaster', 1.5], 'monoculture_warn' => ['Subtle dry rustling of leaves, gentle warning tone', 0.8], 'year_advance' => ['Soft mechanical calendar page flipping with gentle clock chime', 1.0], 'unlock_crop' => ['Bright magical chime, discovery sound, hopeful and brief', 1.0], 'negative_balance' => ['Sad descending two-note tone, very subtle, regretful', 0.7], 'end_report_open' => ['Triumphant ascending chime, festive but warm, harvest celebration', 1.5], 'reset' => ['Soft swoosh, gentle reset, brief and clean', 0.5], // Per-Crop Sounds (10) — beim Pflanzen abgespielt statt generischem 'plant' 'crop_wheat' => ['Soft rustling of wheat stalks in a gentle summer breeze, peaceful golden field', 0.8], 'crop_banana' => ['Tropical banana leaves rustling with distant exotic bird calls, jungle ambience', 0.8], 'crop_reindeer' => ['A single reindeer grunting softly with light antler clicking, cold arctic air', 0.9], 'crop_camel' => ['A single camel bellowing and grunting once, dry desert ambience', 1.0], 'crop_rice' => ['Water gently splashing in a flooded rice paddy, peaceful Asian field with crickets', 0.9], 'crop_coffee' => ['Soft tropical highland breeze with light leaves rustling and distant insects', 0.8], 'crop_dates' => ['Dry palm fronds rustling in a warm desert oasis breeze', 0.8], 'crop_corn' => ['Tall corn stalks rustling in a strong summer wind, big open field', 0.8], 'crop_fish' => ['Single fish splash, water surface ripple, brief and clean', 0.6], 'crop_olive' => ['Mediterranean olive grove with leaves rustling and distant cicadas, warm dry day', 0.9], ]; $generated = 0; $skipped = 0; $failed = []; foreach ($SOUNDS as $name => [$prompt, $duration]) { if ($only !== null && !in_array($name, $only)) continue; $path = "$OUT_DIR/$name.mp3"; if (file_exists($path) && !$force) { echo " [skip] $name.mp3 existiert\n"; $skipped++; continue; } echo " [generate] $name.mp3 ($duration s) ... "; $ch = curl_init('https://api.elevenlabs.io/v1/sound-generation'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'xi-api-key: ' . $KEY, 'Content-Type: application/json', 'Accept: audio/mpeg', ], CURLOPT_POSTFIELDS => json_encode([ 'text' => $prompt, 'duration_seconds' => $duration, 'prompt_influence' => 0.4, ]), CURLOPT_TIMEOUT => 60, ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); if ($status !== 200 || $err) { echo "FAIL (HTTP $status: " . substr($body ?: $err, 0, 200) . ")\n"; $failed[] = $name; } else { file_put_contents($path, $body); $size = round(filesize($path) / 1024, 1); echo "OK ({$size} KB)\n"; $generated++; } // Rate-Limit-Pause usleep(800000); // 0.8s zwischen Calls } echo "\n"; echo "Generiert: $generated · Übersprungen: $skipped · Fehlgeschlagen: " . count($failed) . "\n"; if (!empty($failed)) echo "Failed: " . implode(', ', $failed) . "\n";