Stimmen-Skript auf offizielles ElevenLabs-SDK und eleven_v4 umgestellt (Sprache je Aufnahme, Schlüssel aus .env.local)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MyLusGH2U5AudjoARey3N
This commit is contained in:
Lara 2026-11-02 22:14:31 +01:00
parent 1db1001dfc
commit 5091887695
4 changed files with 109 additions and 10 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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": "",

View file

@ -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/<sprache>/ (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<string, unknown>;
};
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++;