Compare commits
2 Commits
985119bb03
...
96ae76f295
| Author | SHA1 | Date | |
|---|---|---|---|
| 96ae76f295 | |||
| f5b69a9213 |
@@ -599,9 +599,16 @@ async function migrate() {
|
|||||||
INSERT INTO tts_settings (language, voice_id) VALUES
|
INSERT INTO tts_settings (language, voice_id) VALUES
|
||||||
('de', 'rKiu7lQ4c5P3az3745s3'),
|
('de', 'rKiu7lQ4c5P3az3745s3'),
|
||||||
('en', 'cVd39cx0VtXNC13y5Y7z'),
|
('en', 'cVd39cx0VtXNC13y5Y7z'),
|
||||||
('sv', 'XXCqsM8I9KhqA7jLGj1U')
|
('sv', 'XB0fDUnXU5powFXDhCwa')
|
||||||
ON CONFLICT (language) DO NOTHING
|
ON CONFLICT (language) DO NOTHING
|
||||||
`).catch(() => {});
|
`).catch(() => {});
|
||||||
|
// Defekte sv-Seed-Voice ersetzen: 'XXCqsM8I9KhqA7jLGj1U' existiert bei ElevenLabs nicht
|
||||||
|
// (voice_not_found) — dadurch schlug jede schwedische Audio-Generierung fehl.
|
||||||
|
// 'XB0fDUnXU5powFXDhCwa' = Premade-Voice "Charlotte" (schwedischer Akzent), in jedem Account verfügbar.
|
||||||
|
await query(`
|
||||||
|
UPDATE tts_settings SET voice_id = 'XB0fDUnXU5powFXDhCwa'
|
||||||
|
WHERE language = 'sv' AND voice_id = 'XXCqsM8I9KhqA7jLGj1U'
|
||||||
|
`).catch(() => {});
|
||||||
|
|
||||||
// ── Content-Pipeline: Job-Tracking direkt auf der Picture-Zeile ──────────────
|
// ── Content-Pipeline: Job-Tracking direkt auf der Picture-Zeile ──────────────
|
||||||
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_status TEXT NOT NULL DEFAULT 'none'`).catch(() => {});
|
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_status TEXT NOT NULL DEFAULT 'none'`).catch(() => {});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { query } = require('../db');
|
|||||||
const { LANGS, fillMissingRow } = require('./translate');
|
const { LANGS, fillMissingRow } = require('./translate');
|
||||||
const { translateWordGroup } = require('./pairContent');
|
const { translateWordGroup } = require('./pairContent');
|
||||||
const { generatePairsForObject, persistPair } = require('./generatePairs');
|
const { generatePairsForObject, persistPair } = require('./generatePairs');
|
||||||
const { generateAndStore } = require('../routes/audios');
|
const { generateAndStore, describeError } = require('../routes/audios');
|
||||||
|
|
||||||
const queue = [];
|
const queue = [];
|
||||||
let running = false;
|
let running = false;
|
||||||
@@ -167,7 +167,7 @@ async function runPicture(pictureId) {
|
|||||||
await generateWithBackoff(u);
|
await generateWithBackoff(u);
|
||||||
progress.audiosDone++;
|
progress.audiosDone++;
|
||||||
} catch (err) {
|
} 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);
|
await setStep(pictureId, 'audio', progress);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ async function generateAndStore({ text, voice_id, language, model_id, speed, sta
|
|||||||
return result.rows[0];
|
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 ───────────
|
// ── 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.
|
// 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.
|
// 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++;
|
results.generated++;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
results.failed++;
|
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);
|
res.json(results);
|
||||||
@@ -318,3 +324,4 @@ router.delete('/:id', async (req, res, next) => {
|
|||||||
module.exports = router;
|
module.exports = router;
|
||||||
// Für lib/pipeline.js (Audio-Generierung außerhalb des HTTP-Kontexts)
|
// Für lib/pipeline.js (Audio-Generierung außerhalb des HTTP-Kontexts)
|
||||||
module.exports.generateAndStore = generateAndStore;
|
module.exports.generateAndStore = generateAndStore;
|
||||||
|
module.exports.describeError = describeError;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { query } = require('../db');
|
|||||||
const { LANGS } = require('../lib/translate');
|
const { LANGS } = require('../lib/translate');
|
||||||
const { loadPairContext, computeReadiness, loadPairContent } = require('../lib/pairContent');
|
const { loadPairContext, computeReadiness, loadPairContent } = require('../lib/pairContent');
|
||||||
const { enqueue, loadPairs, collectAudioUnits, generateWithBackoff, translatePair } = require('../lib/pipeline');
|
const { enqueue, loadPairs, collectAudioUnits, generateWithBackoff, translatePair } = require('../lib/pipeline');
|
||||||
|
const { describeError } = require('./audios');
|
||||||
const { PLACEHOLDER_RE } = require('../lib/placeholders');
|
const { PLACEHOLDER_RE } = require('../lib/placeholders');
|
||||||
|
|
||||||
// ── Objekt-Wort-Erkennung in Sätzen (für die manuelle Zuweisung beim Review) ──
|
// ── 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++; }
|
try { await generateWithBackoff(u); result.generated++; }
|
||||||
catch (err) {
|
catch (err) {
|
||||||
result.failed++;
|
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);
|
res.json(result);
|
||||||
|
|||||||
@@ -3,6 +3,21 @@ const { query } = require('../db');
|
|||||||
|
|
||||||
const EDITABLE = ['voice_id', 'model_id', 'speed', 'stability', 'similarity_boost', 'style'];
|
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
|
// GET /api/tts-settings
|
||||||
router.get('/', async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
// Default ElevenLabs voice per language (ISO 639-1 → voice_id).
|
// Default ElevenLabs voice per language (ISO 639-1 → voice_id).
|
||||||
// Configure via env (ELEVENLABS_VOICE_DE/EN/SV). Falls back to a shared multilingual voice.
|
// Configure via env (ELEVENLABS_VOICE_DE/EN/SV). Falls back to a shared multilingual voice.
|
||||||
|
|
||||||
const FALLBACK_VOICE = process.env.ELEVENLABS_VOICE_DEFAULT || 'XXCqsM8I9KhqA7jLGj1U';
|
// 'EXAVITQu4vr4xnSDxMaL' = ElevenLabs-Premade "Sarah" — existiert in jedem Account.
|
||||||
|
// (Der frühere Default 'XXCqsM8I9KhqA7jLGj1U' lieferte voice_not_found.)
|
||||||
|
const FALLBACK_VOICE = process.env.ELEVENLABS_VOICE_DEFAULT || 'EXAVITQu4vr4xnSDxMaL';
|
||||||
|
|
||||||
const VOICES = {
|
const VOICES = {
|
||||||
de: process.env.ELEVENLABS_VOICE_DE || FALLBACK_VOICE,
|
de: process.env.ELEVENLABS_VOICE_DE || FALLBACK_VOICE,
|
||||||
en: process.env.ELEVENLABS_VOICE_EN || FALLBACK_VOICE,
|
en: process.env.ELEVENLABS_VOICE_EN || FALLBACK_VOICE,
|
||||||
sv: process.env.ELEVENLABS_VOICE_SV || FALLBACK_VOICE,
|
// Premade "Charlotte" (schwedischer Akzent) als Default für Schwedisch
|
||||||
|
sv: process.env.ELEVENLABS_VOICE_SV || 'XB0fDUnXU5powFXDhCwa',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Returns the configured voice_id for a language code (default: fallback voice). */
|
/** Returns the configured voice_id for a language code (default: fallback voice). */
|
||||||
|
|||||||
Reference in New Issue
Block a user