- Neuer Mitarbeiter Komponist (.claude/agents/komponist.md) - Liederbuch: Plan + Lieder 1, 2, 3, 4, 5, 12 (drehbuecher/lieder/) - Suno: feste Stimme aus dem Favoriten, Stilvorlage, ausgewählte Fassungen (suno-auswahl.md) - Suno-Lizenzantwort für Noa in der Mitte (wissen/) - Werkzeuge für Melodie-Skizzen und Leseseite - Laufende Remotion-, Stimmen- und Figurenarbeiten des Tages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018MyLus898ZL9jjJJDvvxa8
61 lines
2.7 KiB
TypeScript
61 lines
2.7 KiB
TypeScript
// Erzeugt die Hintergrundmusik einer Folge mit ElevenLabs Music (instrumental,
|
|
// sprachunabhängig). Die Stücke stehen in src/folgeNN/musik.json
|
|
// ({name: {sekunden, beschreibung}}), die Dateien landen in
|
|
// public/musik/folgeNN/<name>.mp3 (nicht im Git). Nur fehlende oder geänderte
|
|
// Stücke werden neu erzeugt; welche Szene welches Stück spielt, steht in FolgeNN.tsx.
|
|
//
|
|
// npx tsx werkzeuge/musik.mts F01
|
|
// … F01 --stueck lied nur ein Stück
|
|
// … F01 --neu alle neu erzeugen
|
|
// … F01 --probe nur anzeigen (kostet nichts)
|
|
|
|
import fs from 'node:fs';
|
|
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 kuerzel = args.find((a) => !a.startsWith('--'));
|
|
const wert = (n: string) => (args.includes(n) ? args[args.indexOf(n) + 1] : undefined);
|
|
const nurStueck = wert('--stueck');
|
|
const neu = args.includes('--neu');
|
|
const probe = args.includes('--probe');
|
|
if (!kuerzel) throw new Error('Folge angeben, z. B. F01');
|
|
|
|
const nummer = kuerzel.replace(/\D/g, '');
|
|
const stuecke = JSON.parse(fs.readFileSync(`src/folge${nummer}/musik.json`, 'utf8')) as Record<string, {sekunden: number; beschreibung: string}>;
|
|
const ordner = `public/musik/folge${nummer}`;
|
|
fs.mkdirSync(ordner, {recursive: true});
|
|
// Merkt sich, mit welcher Beschreibung ein Stück erzeugt wurde
|
|
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();
|
|
for (const [name, s] of Object.entries(stuecke)) {
|
|
if (nurStueck && name !== nurStueck) continue;
|
|
const pfad = path.join(ordner, `${name}.mp3`);
|
|
const kennung = `${s.sekunden}|${s.beschreibung}`;
|
|
if (!neu && erzeugt[name] === kennung && fs.existsSync(pfad)) continue;
|
|
if (probe) {
|
|
console.log(`~ ${name} (${s.sekunden} s): ${s.beschreibung}`);
|
|
continue;
|
|
}
|
|
const audio = await client!.music.compose({
|
|
prompt: s.beschreibung,
|
|
musicLengthMs: Math.round(s.sekunden * 1000),
|
|
forceInstrumental: true,
|
|
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));
|
|
erzeugt[name] = kennung;
|
|
fs.writeFileSync(merkDatei, JSON.stringify(erzeugt, null, '\t') + '\n');
|
|
console.log(`✓ ${name} → ${pfad}`);
|
|
}
|