diff --git a/remotion/package-lock.json b/remotion/package-lock.json index 9522501..fd34a75 100644 --- a/remotion/package-lock.json +++ b/remotion/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "pino-remotion", "dependencies": { + "@elevenlabs/elevenlabs-js": "^2.75.0", "@remotion/cli": "^4.0.300", "@remotion/google-fonts": "^4.0.300", "react": "^18.3.1", @@ -32,6 +33,41 @@ "node": ">=6.0.0" } }, + "node_modules/@elevenlabs/elevenlabs-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@elevenlabs/elevenlabs-js/-/elevenlabs-js-2.75.0.tgz", + "integrity": "sha512-sCEddZAQS+JdAvD+jnqXund75mygRkHzgigqbWsQpcJozwe15FOtymS2+28YMOY06Pmja9l8EUE1LlXAtlW0Nw==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.9", + "node-fetch": "^2.7.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@elevenlabs/elevenlabs-js/node_modules/ws": { + "version": "8.22.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.1.tgz", + "integrity": "sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -1276,6 +1312,12 @@ "node": ">=6.0" } }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2117,6 +2159,26 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.60", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", @@ -2745,6 +2807,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3295,6 +3363,12 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.112.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.112.2.tgz", @@ -3413,6 +3487,16 @@ } } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/remotion/package.json b/remotion/package.json index 522ac77..904a7a5 100644 --- a/remotion/package.json +++ b/remotion/package.json @@ -6,6 +6,7 @@ "render": "remotion render" }, "dependencies": { + "@elevenlabs/elevenlabs-js": "^2.75.0", "@remotion/cli": "^4.0.300", "@remotion/google-fonts": "^4.0.300", "react": "^18.3.1", diff --git a/remotion/werkzeuge/stimmen.config.json b/remotion/werkzeuge/stimmen.config.json index 93f5910..cc659af 100644 --- a/remotion/werkzeuge/stimmen.config.json +++ b/remotion/werkzeuge/stimmen.config.json @@ -1,7 +1,10 @@ { - "modell": "eleven_multilingual_v2", + "modell": "eleven_v4", "format": "mp3_44100_128", - "einstellungen": {"stability": 0.5, "similarity_boost": 0.75, "style": 0.2}, + "einstellungen": { + "stability": 0.5, + "similarity_boost": 0.75 + }, "stimmen": { "ERZ": "", "PINO": "", diff --git a/remotion/werkzeuge/stimmen.mts b/remotion/werkzeuge/stimmen.mts index 9610d54..b37a846 100644 --- a/remotion/werkzeuge/stimmen.mts +++ b/remotion/werkzeuge/stimmen.mts @@ -8,7 +8,8 @@ // … F02 --probe nur anzeigen, was erzeugt würde (kostet nichts) // … F02 --sprache es andere Sprache (de, es, fr; ohne Angabe de) // -// Der Schlüssel kommt nur aus der Umgebungsvariable, nie aus einer Datei im Repo. +// Der Schlüssel kommt aus ELEVENLABS_API_KEY bzw. aus youtube/.env.local (angelegt mit +// `npx -y @elevenlabs/cli@latest onboard init`, nicht im Git) – nie aus dem Code. // Stimmen-IDs und Modell stehen in werkzeuge/stimmen.config.json (je Sprache // lassen sich dort unter „sprachen“ eigene Stimmen eintragen). // Aufnahmen landen in public/stimmen/folgeNN// (nicht im Git; auf dem VPS ein @@ -19,9 +20,14 @@ import {spawnSync} from 'node:child_process'; 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); +// Schlüssel aus der .env.local im Repo-Wurzelordner laden (falls nicht schon gesetzt) +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('--')); @@ -42,7 +48,8 @@ const cfg = JSON.parse(fs.readFileSync('werkzeuge/stimmen.config.json', 'utf8')) einstellungen?: Record; }; const schluessel = process.env.ELEVENLABS_API_KEY; -if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt.'); +if (!schluessel && !probe) throw new Error('ELEVENLABS_API_KEY ist nicht gesetzt (siehe youtube/.env.local).'); +const client = probe ? null : new ElevenLabsClient(); // liest ELEVENLABS_API_KEY const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'; const ausgabe = spawnSync(npx, ['tsx', 'werkzeuge/szenenliste.mts', `./src/folge${nummer}/Folge${nummer}.tsx`, kuerzel], {encoding: 'utf8', shell: process.platform === 'win32', env: {...process.env, SPRACHE: sprache}}); @@ -91,13 +98,17 @@ for (const s of szenen) { console.log(`~ ${schl} (${z.wer}): ${z.text}`); continue; } - const antwort = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${stimme}?output_format=${cfg.format}`, { - method: 'POST', - headers: {'xi-api-key': schluessel!, 'Content-Type': 'application/json', Accept: 'audio/mpeg'}, - body: JSON.stringify({text: z.text, model_id: cfg.modell, voice_settings: cfg.einstellungen}), + // Sprache festlegen (bessere Aussprache); eleven_v4 kennt nur stability und similarity_boost + const audio = await client!.textToSpeech.convert(stimme, { + text: z.text, + modelId: cfg.modell, + languageCode: sprache, + outputFormat: cfg.format as never, + voiceSettings: {stability: cfg.einstellungen?.stability as number, similarityBoost: cfg.einstellungen?.similarity_boost as number}, }); - if (!antwort.ok) throw new Error(`${schl}: ElevenLabs antwortet ${antwort.status} ${await antwort.text()}`); - fs.writeFileSync(pfad, Buffer.from(await antwort.arrayBuffer())); + const teile: Buffer[] = []; + for await (const t of Readable.fromWeb(audio as never)) teile.push(Buffer.from(t)); + fs.writeFileSync(pfad, Buffer.concat(teile)); aufnahmen[schl] = {datei, dauer: laenge(pfad), text: z.text}; fs.writeFileSync(jsonDatei, JSON.stringify(aufnahmen, null, '\t') + '\n'); erzeugt++;