feat: Placeholder in der Auto-Generierung + Token-Leak-Fix
- Pair-Generierung markiert Nomen per [surface|lemma]-Markup und löst sie zu
{{label.o:objectId}} / {{label.w:wordId}} auf (Words werden auto-erstellt)
- Pipeline übersetzt + vertont Placeholder-Wörter aus den Sätzen mit
- translateText halluziniert keine ⟦PHn⟧-Tokens mehr (kein Token-Prompt ohne
Tokens, defensives Strippen); TTS/Review lösen geleakte Tokens auf
- POST /api/pipeline/repair-tokens repariert bestehende Sätze + Audios
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ const TRANSLATE_CONFIG = {
|
||||
|
||||
// ── Placeholder-Schutz ────────────────────────────────────────────────────────
|
||||
// Format im Quelltext: {{label.w:uuid}} oder {{label.o:uuid}}
|
||||
const { PLACEHOLDER_RE } = require('./placeholders');
|
||||
const { PLACEHOLDER_RE, stripLeakedTokens } = require('./placeholders');
|
||||
|
||||
// Sätze für Claude vorbereiten: jedes Placeholder durch ⟦PHn:label⟧-Token ersetzen.
|
||||
// Token-Format ist absichtlich exotisch, damit Claude es nicht versehentlich ändert.
|
||||
@@ -98,17 +98,24 @@ async function translateText({ text, from, to }) {
|
||||
if (!text || !text.trim()) return '';
|
||||
const { tokenized, tokens } = tokenize(text);
|
||||
const system = 'Du bist ein professioneller Übersetzer. Antworte AUSSCHLIESSLICH mit gültigem JSON, ohne Markdown, ohne Erklärungen.';
|
||||
const user = `Übersetze diesen Text von ${LANG_LABEL[from] || from} nach ${LANG_LABEL[to] || to}.\n\n` +
|
||||
`WICHTIG: Tokens der Form ⟦PHn:wort⟧ sind Platzhalter. Übersetze NUR das Wort innerhalb des Tokens, ` +
|
||||
`behalte das Token-Format exakt bei (⟦PHn:übersetztesWort⟧). Passe die Beugung des Wortes an den umgebenden Satz an ` +
|
||||
`(Mehrzahl/Kasus). Die Token-Reihenfolge im Satz darfst du frei wählen wie es natürlich klingt.\n\n` +
|
||||
`Quelltext:\n${tokenized}\n\n` +
|
||||
`Antwort-Format:\n{"translated":"...","labels":{${tokens.map(t => `"${t.key}":"<übersetztes Wort>"`).join(',')}}}`;
|
||||
// Token-Erklärung NUR wenn der Text wirklich Tokens enthält — sonst halluziniert
|
||||
// Claude gelegentlich ⟦PHn:…⟧-Tokens in die Übersetzung hinein.
|
||||
const user = tokens.length
|
||||
? `Übersetze diesen Text von ${LANG_LABEL[from] || from} nach ${LANG_LABEL[to] || to}.\n\n` +
|
||||
`WICHTIG: Tokens der Form ⟦PHn:wort⟧ sind Platzhalter. Übersetze NUR das Wort innerhalb des Tokens, ` +
|
||||
`behalte das Token-Format exakt bei (⟦PHn:übersetztesWort⟧). Passe die Beugung des Wortes an den umgebenden Satz an ` +
|
||||
`(Mehrzahl/Kasus). Die Token-Reihenfolge im Satz darfst du frei wählen wie es natürlich klingt.\n\n` +
|
||||
`Quelltext:\n${tokenized}\n\n` +
|
||||
`Antwort-Format:\n{"translated":"...","labels":{${tokens.map(t => `"${t.key}":"<übersetztes Wort>"`).join(',')}}}`
|
||||
: `Übersetze diesen Text von ${LANG_LABEL[from] || from} nach ${LANG_LABEL[to] || to}.\n\n` +
|
||||
`Quelltext:\n${tokenized}\n\n` +
|
||||
`Antwort-Format:\n{"translated":"..."}`;
|
||||
|
||||
const data = await callClaude({ system, user });
|
||||
if (typeof data.translated !== 'string') throw new Error('Ungültiges JSON: translated fehlt');
|
||||
const { text: detok } = detokenize(data.translated, tokens, data.labels || {});
|
||||
return detok;
|
||||
// Defensiv: von Claude erfundene/umnummerierte Tokens dürfen nie in die DB
|
||||
return stripLeakedTokens(detok);
|
||||
}
|
||||
|
||||
// ── Auto-Status für Wörter (Spiegel zum Trigger in words.js) ──────────────────
|
||||
|
||||
Reference in New Issue
Block a user