fix: Readiness pro answer_type + Objekt-Zuweisung & Audio-Nachholen im Publish-Flow

- computeReadiness: yes_no braucht keinen Positiv-Satz (nur answer-Flag),
  word-Pairs prüfen verlinkte Wörter (Titel + Audio) statt Statement-Sätze
  → behebt 'bei jedem Pair fehlt ein Audio' im Publish-Review
- Bundle liefert Placeholder-Kandidaten: Objekt-Wörter, die im deutschen
  Satz vorkommen (außerhalb bestehender Placeholder, inkl. Flexion)
- POST /pipeline/assign-object: Wort in allen 3 Sprachen als
  {{wort.o:objectId}} markieren (über die Wort-Übersetzungen)
- POST /pipeline/picture/:id/audio-fill: fehlende Audios nachgenerieren

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 21:48:03 +02:00
parent 6af2428df5
commit fb93d2296e
3 changed files with 178 additions and 18 deletions

View File

@@ -25,6 +25,22 @@ async function loadPairContext(pairs, lang) {
FROM statements WHERE id = ANY($1)`, [statementIds]);
r.rows.forEach(s => { statementsMap[s.id] = s; });
}
// Wörter pro Statement (für word-Pairs): Titel in der Sprache + Audio-Check
const statementWordsMap = {}; // statementId → [{ id, titel }]
const wordIds = new Set();
if (statementIds.length) {
for (const link of ['statement_positive_words', 'statement_negative_words']) {
const r = await query(
`SELECT lw.statement_id, w.id, w.titel_${lang} AS titel
FROM ${link} lw JOIN words w ON w.id = lw.word_id
WHERE lw.statement_id = ANY($1)`, [statementIds]);
r.rows.forEach(row => {
(statementWordsMap[row.statement_id] ??= []).push({ id: row.id, titel: row.titel });
wordIds.add(row.id);
});
}
}
if (pairIds.length) {
const r = await query(
`SELECT op.pair_id,
@@ -37,7 +53,7 @@ async function loadPairContext(pairs, lang) {
GROUP BY op.pair_id`, [pairIds]);
r.rows.forEach(row => { pictureMap[row.pair_id] = row; });
const ids = [...questionIds, ...statementIds];
const ids = [...questionIds, ...statementIds, ...wordIds];
if (ids.length) {
const a = await query(
`SELECT source_table, source_id, source_field FROM audios
@@ -45,15 +61,19 @@ async function loadPairContext(pairs, lang) {
a.rows.forEach(x => { audioMap[`${x.source_table}|${x.source_id}|${x.source_field}`] = true; });
}
}
return { questionsMap, statementsMap, pictureMap, audioMap };
return { questionsMap, statementsMap, pictureMap, audioMap, statementWordsMap };
}
// Berechnet, was einem Pair zur Veröffentlichung (für eine Sprache) noch fehlt.
// Berechnet, was einem Pair zur Veröffentlichung (für eine Sprache) noch fehlt
// abhängig vom answer_type: yes_no/word-Pairs haben konstruktionsbedingt keine
// Statement-Sätze (nur answer-Flag bzw. verlinkte Wörter) und dürfen dafür
// nicht als unvollständig gelten.
// opts.skipPicturePublished: Bild-Publish-Check überspringen (Bundle-Publish veröffentlicht
// das Bild im selben Schritt), Bild-Existenz wird weiter geprüft.
// opts.skipStatusChecks: Publish-Status von Frage/Statements ignorieren (werden mitveröffentlicht).
function computeReadiness(p, ctx, lang, opts = {}) {
const missing = [];
const type = p.answer_type;
const q = p.question_id ? ctx.questionsMap[p.question_id] : null;
const ps = p.positive_statement_id ? ctx.statementsMap[p.positive_statement_id] : null;
const ns = p.negative_statement_id ? ctx.statementsMap[p.negative_statement_id] : null;
@@ -71,19 +91,32 @@ function computeReadiness(p, ctx, lang, opts = {}) {
if (!ctx.audioMap[`questions|${p.question_id}|sentence`]) missing.push('Audio Frage fehlt');
}
}
// Positiv-Statement
if (ps) {
if (!(ps.positive || '').trim()) missing.push(`Positiv-Satz (${lang}) fehlt`);
else {
if (!opts.skipStatusChecks && ps.status !== 'published') missing.push('Positiv-Satz nicht freigegeben');
if (!ctx.audioMap[`statements|${p.positive_statement_id}|positive_sentence`]) missing.push('Audio Positiv fehlt');
if (type === 'word') {
// Inhalt sind die verlinkten Wörter: Titel + Audio in der Sprache
for (const [stmtId, label] of [[p.positive_statement_id, 'Positiv'], [p.negative_statement_id, 'Negativ']]) {
if (!stmtId) continue;
for (const w of (ctx.statementWordsMap?.[stmtId] || [])) {
if (!(w.titel || '').trim()) { missing.push(`${label}-Wort (${lang}) fehlt`); continue; }
if (!ctx.audioMap[`words|${w.id}|titel`]) missing.push(`Audio ${label}-Wort „${w.titel}" fehlt`);
}
}
} else if (type !== 'yes_no') {
// text / question: Positiv-Satz Pflicht
if (ps) {
if (!(ps.positive || '').trim()) missing.push(`Positiv-Satz (${lang}) fehlt`);
else {
if (!opts.skipStatusChecks && ps.status !== 'published') missing.push('Positiv-Satz nicht freigegeben');
if (!ctx.audioMap[`statements|${p.positive_statement_id}|positive_sentence`]) missing.push('Audio Positiv fehlt');
}
}
// Negativ-Statement (nur wenn Text vorhanden)
if (ns && (ns.negative || '').trim()) {
if (!opts.skipStatusChecks && ns.status !== 'published') missing.push('Negativ-Satz nicht freigegeben');
if (!ctx.audioMap[`statements|${p.negative_statement_id}|negative_sentence`]) missing.push('Audio Negativ fehlt');
}
}
// Negativ-Statement (nur wenn Text vorhanden)
if (ns && (ns.negative || '').trim()) {
if (!opts.skipStatusChecks && ns.status !== 'published') missing.push('Negativ-Satz nicht freigegeben');
if (!ctx.audioMap[`statements|${p.negative_statement_id}|negative_sentence`]) missing.push('Audio Negativ fehlt');
}
// yes_no: Positiv-Statement trägt nur die Ja/Nein-Antwort — kein Satz, kein Audio nötig.
return { missing, missingCount: missing.length, ready: missing.length === 0 };
}

View File

@@ -334,4 +334,4 @@ async function generateWithBackoff(u) {
}
}
module.exports = { enqueue, resumePending };
module.exports = { enqueue, resumePending, loadPairs, collectAudioUnits, generateWithBackoff };