youtube/remotion/werkzeuge/geraeusche.mts
Lara 0e34d998a5 Folge 1: Geräusche synchron, Lied in DE/ES/FR gesungen (Suno)
- 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>
2026-11-04 12:54:39 +01:00

72 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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`);