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

View File

@@ -603,6 +603,38 @@ async function migrate() {
ON CONFLICT (language) DO NOTHING
`).catch(() => {});
// ── 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 DROP CONSTRAINT IF EXISTS pictures_pipeline_status_check`).catch(() => {});
await query(`
ALTER TABLE pictures ADD CONSTRAINT pictures_pipeline_status_check
CHECK (pipeline_status IN ('none', 'queued', 'running', 'failed', 'ready', 'published'))
`).catch(() => {});
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_step TEXT`).catch(() => {});
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_progress JSONB`).catch(() => {});
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_error TEXT`).catch(() => {});
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_started_at TIMESTAMPTZ`).catch(() => {});
await query(`ALTER TABLE pictures ADD COLUMN IF NOT EXISTS pipeline_finished_at TIMESTAMPTZ`).catch(() => {});
// app_settings — generischer Key/Value-Store (JSONB) für Konfiguration
await query(`
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
await query(`
DROP TRIGGER IF EXISTS app_settings_updated_at ON app_settings;
CREATE TRIGGER app_settings_updated_at
BEFORE UPDATE ON app_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at()
`);
await query(`
INSERT INTO app_settings (key, value) VALUES ('pipeline.pairs_per_object', '5'::jsonb)
ON CONFLICT (key) DO NOTHING
`).catch(() => {});
// ── Migrate old {{uuid}} placeholders → new {{label.w:uuid}} / {{label.o:uuid}} ──
await migratePlaceholders();