feat: automatische Content-Pipeline (release → pairs → übersetzen → audio → ready)

- pictures.pipeline_* Spalten + app_settings Tabelle (Migration)
- lib/placeholders.js: Placeholder-Auflösung; TTS spricht keine UUIDs mehr
- lib/pairContent.js: geteilte Pair-Logik (Readiness mit Skip-Optionen)
- lib/generatePairs.js: Claude-Generierung (konfigurierbare Anzahl, nur
  Nomen/Adjektive bei word-Pairs) + serverseitige Persistenz inkl. object_pairs
- lib/pipeline.js: In-Process-Runner, idempotente Schritte, Boot-Resume
- routes/pipeline.js: release/retry/overview/bundle/settings + Bild-Publish
  (kaskadiert Fragen/Statements/Pairs/Wörter/Objekte/Bild)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:52:11 +02:00
parent 29a260e351
commit 6af2428df5
10 changed files with 946 additions and 151 deletions

18
src/lib/placeholders.js Normal file
View File

@@ -0,0 +1,18 @@
// Placeholder-Format in Satz-/Titel-Feldern: {{label.w:uuid}} (Wort) bzw. {{label.o:uuid}} (Objekt).
// Geteilt zwischen translate.js (Token-Schutz) und TTS (Auflösung in Sprechtext).
const PLACEHOLDER_RE = /\{\{([^.{}]+)\.(w|o):([0-9a-f-]{36})\}\}/g;
// Legacy-Form ohne Label: {{uuid}} — sollte migriert sein, defensiv trotzdem entfernen.
const LEGACY_PLACEHOLDER_RE = /\{\{\s*[0-9a-f-]{36}\s*\}\}/g;
// Macht aus "Ist das ein {{Apfel.w:1234-…}}?" → "Ist das ein Apfel?" (für TTS/Anzeige).
function resolvePlaceholdersToLabels(text) {
if (!text) return '';
return String(text)
.replace(PLACEHOLDER_RE, (_, label) => label)
.replace(LEGACY_PLACEHOLDER_RE, '')
.replace(/\s{2,}/g, ' ')
.trim();
}
module.exports = { PLACEHOLDER_RE, resolvePlaceholdersToLabels };