diff --git a/src/lib/pipeline.js b/src/lib/pipeline.js index 4a0e3f1..de53bfd 100644 --- a/src/lib/pipeline.js +++ b/src/lib/pipeline.js @@ -5,7 +5,7 @@ const { query } = require('../db'); const { LANGS, fillMissingRow } = require('./translate'); const { translateWordGroup } = require('./pairContent'); const { generatePairsForObject, persistPair } = require('./generatePairs'); -const { generateAndStore } = require('../routes/audios'); +const { generateAndStore, describeError } = require('../routes/audios'); const queue = []; let running = false; @@ -167,7 +167,7 @@ async function runPicture(pictureId) { await generateWithBackoff(u); progress.audiosDone++; } catch (err) { - failures.push(`${u.source_table}/${u.source_field}/${u.language}: ${err.message}`); + failures.push(`${u.source_table}/${u.source_field}/${u.language}: ${describeError(err)}`); } await setStep(pictureId, 'audio', progress); } diff --git a/src/routes/audios.js b/src/routes/audios.js index c31d8b0..59a5c53 100644 --- a/src/routes/audios.js +++ b/src/routes/audios.js @@ -99,6 +99,12 @@ async function generateAndStore({ text, voice_id, language, model_id, speed, sta return result.rows[0]; } +// Fehlertext inkl. ElevenLabs-Detail (z.B. voice_not_found) — sonst geht die +// eigentliche Ursache in Batch-Ergebnissen als generisches 'ElevenLabs error' unter. +function describeError(err) { + return err.detail ? `${err.message}: ${String(err.detail).slice(0, 300)}` : err.message; +} + // ── Fehlende/vorhandene Audio-Einheiten für einen Filter berechnen ─────────── // Regel: Eine Quell-Zeile+Feld gilt erst dann als vertonbar, wenn ALLE drei Sprachen Text haben. // Wenn nur eine Sprache angefragt ist (Filter), wird trotzdem auf Vollständigkeit aller Sprachen geprüft. @@ -265,7 +271,7 @@ router.post('/generate-batch', async (req, res, next) => { results.generated++; } catch (err) { results.failed++; - results.errors.push({ source_id: u.source_id, field: u.source_field, lang: u.language, error: err.message }); + results.errors.push({ source_id: u.source_id, field: u.source_field, lang: u.language, error: describeError(err) }); } } res.json(results); @@ -318,3 +324,4 @@ router.delete('/:id', async (req, res, next) => { module.exports = router; // Für lib/pipeline.js (Audio-Generierung außerhalb des HTTP-Kontexts) module.exports.generateAndStore = generateAndStore; +module.exports.describeError = describeError; diff --git a/src/routes/pipeline.js b/src/routes/pipeline.js index 72ce913..a2aba70 100644 --- a/src/routes/pipeline.js +++ b/src/routes/pipeline.js @@ -4,6 +4,7 @@ const { query } = require('../db'); const { LANGS } = require('../lib/translate'); const { loadPairContext, computeReadiness, loadPairContent } = require('../lib/pairContent'); const { enqueue, loadPairs, collectAudioUnits, generateWithBackoff, translatePair } = require('../lib/pipeline'); +const { describeError } = require('./audios'); const { PLACEHOLDER_RE } = require('../lib/placeholders'); // ── Objekt-Wort-Erkennung in Sätzen (für die manuelle Zuweisung beim Review) ── @@ -233,7 +234,7 @@ router.post('/picture/:id/audio-fill', async (req, res, next) => { try { await generateWithBackoff(u); result.generated++; } catch (err) { result.failed++; - result.errors.push({ source: `${u.source_table}/${u.source_field}/${u.language}`, error: err.message }); + result.errors.push({ source: `${u.source_table}/${u.source_field}/${u.language}`, error: describeError(err) }); } } res.json(result); diff --git a/src/routes/tts-settings.js b/src/routes/tts-settings.js index 1a6532f..e2f703b 100644 --- a/src/routes/tts-settings.js +++ b/src/routes/tts-settings.js @@ -3,6 +3,21 @@ const { query } = require('../db'); const EDITABLE = ['voice_id', 'model_id', 'speed', 'stability', 'similarity_boost', 'style']; +// GET /api/tts-settings/voices/available — Stimmen des ElevenLabs-Accounts. +// Muss vor GET /:language registriert sein, sonst matcht ':language' den Pfad. +router.get('/voices/available', async (req, res, next) => { + try { + const apiKey = process.env.ELEVENLABS_API_KEY; + if (!apiKey) return res.status(500).json({ error: 'ELEVENLABS_API_KEY not configured' }); + const r = await fetch('https://api.elevenlabs.io/v1/voices', { headers: { 'xi-api-key': apiKey } }); + if (!r.ok) return res.status(r.status).json({ error: 'ElevenLabs error', detail: await r.text() }); + const data = await r.json(); + res.json((data.voices || []).map(v => ({ + voice_id: v.voice_id, name: v.name, labels: v.labels || {}, preview_url: v.preview_url || null, + }))); + } catch (err) { next(err); } +}); + // GET /api/tts-settings router.get('/', async (req, res, next) => { try {