- Geräusche (baukasten/geraeusche, werkzeuge/geraeusche.mts): Stille am Anfang abgeschnitten, F01-Töne auf Bildereignissen und leiser; Ton.laenge blendet aus - Szene 6: Blume und Noas Arme gehen früher runter - Lied: je Sprache eigenes Suno-Lied (SUNO_LIEDER in lied.tsx), ES/FR-Zeiten per faster-whisper gemessen (+0,45 s, am DE-Lied geeicht); Ton in public/musik/lieder (nicht im Git) - F02 DE vertont Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.3 KiB
TypeScript
72 lines
3.3 KiB
TypeScript
// 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/<name>.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<string, Eintrag>;
|
||
const ordner = 'public/geraeusche';
|
||
fs.mkdirSync(ordner, {recursive: true});
|
||
const merkDatei = path.join(ordner, 'erzeugt.json');
|
||
const erzeugt: Record<string, string> = 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`);
|