// Erzeugt Geräusche und Hintergrundklänge (Atmo) mit ElevenLabs Sound Effects // (sprachunabhängig, für alle Folgen). Die Klänge stehen in src/baukasten/geraeusche.json // ({name: {sekunden, beschreibung, loop?}}), die Dateien landen in // public/geraeusche/.mp3 (nicht im Git). Nur fehlende oder geänderte // Klänge werden neu erzeugt; wo sie erklingen, steht in FolgeNN.tsx (mitKlang). // // npx tsx werkzeuge/geraeusche.mts // … --klang malen nur einen Klang // … --neu alle neu erzeugen // … --probe nur anzeigen (kostet nichts) import {execFileSync} from 'node:child_process'; import fs from 'node:fs'; // Einzelgeräusche beginnen bei ElevenLabs oft mit bis zu 1 s Stille – abschneiden, // damit `versatz` in FolgeNN.tsx genau den Einsatz des Geräuschs trifft. export const stilleWeg = (pfad: string) => { const tmp = pfad.replace(/\.mp3$/, '.tmp.mp3'); execFileSync('ffmpeg', ['-hide_banner', '-loglevel', 'error', '-y', '-i', pfad, '-af', 'silenceremove=start_periods=1:start_threshold=-40dB', '-ar', '48000', '-b:a', '128k', tmp]); fs.renameSync(tmp, pfad); }; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {Readable} from 'node:stream'; import {ElevenLabsClient} from '@elevenlabs/elevenlabs-js'; const WURZEL = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); process.chdir(WURZEL); const envDatei = path.join(WURZEL, '..', '.env.local'); if (!process.env.ELEVENLABS_API_KEY && fs.existsSync(envDatei)) process.loadEnvFile(envDatei); const args = process.argv.slice(2); const wert = (n: string) => (args.includes(n) ? args[args.indexOf(n) + 1] : undefined); const nurKlang = wert('--klang'); const neu = args.includes('--neu'); const probe = args.includes('--probe'); type Eintrag = {sekunden: number; beschreibung: string; loop?: boolean}; const klaenge = JSON.parse(fs.readFileSync('src/baukasten/geraeusche.json', 'utf8')) as Record; const ordner = 'public/geraeusche'; fs.mkdirSync(ordner, {recursive: true}); const merkDatei = path.join(ordner, 'erzeugt.json'); const erzeugt: Record = fs.existsSync(merkDatei) ? JSON.parse(fs.readFileSync(merkDatei, 'utf8')) : {}; const client = probe ? null : new ElevenLabsClient(); let sekunden = 0; for (const [name, k] of Object.entries(klaenge)) { if (nurKlang && name !== nurKlang) continue; const pfad = path.join(ordner, `${name}.mp3`); const kennung = `${k.sekunden}|${k.loop ? 'loop|' : ''}${k.beschreibung}`; if (!neu && erzeugt[name] === kennung && fs.existsSync(pfad)) continue; sekunden += k.sekunden; if (probe) { console.log(`~ ${name} (${k.sekunden} s${k.loop ? ', Schleife' : ''}): ${k.beschreibung}`); continue; } const audio = await client!.textToSoundEffects.convert({ text: k.beschreibung, durationSeconds: k.sekunden, promptInfluence: 0.5, loop: k.loop ?? false, outputFormat: 'mp3_44100_128' as never, }); const teile: Buffer[] = []; for await (const t of Readable.fromWeb(audio as never)) teile.push(Buffer.from(t)); fs.writeFileSync(pfad, Buffer.concat(teile)); if (!k.loop) stilleWeg(pfad); erzeugt[name] = kennung; fs.writeFileSync(merkDatei, JSON.stringify(erzeugt, null, '\t') + '\n'); console.log(`✓ ${name} → ${pfad}`); } console.log(`\n${probe ? 'Würde erzeugen' : 'Erzeugt'}: ${sekunden} Sekunden`);