fix: Übersetzungs-Retry + robuster Translate-Step + Nachhol-Endpoints

- callClaude: Retry mit Backoff bei Überlast/Rate-Limit/Netzfehler
  (429/500/503/529) — wahrscheinliche Ursache der fehlenden SV-Übersetzung
- Translate-Step pro Pair gekapselt: ein Fehler reißt nicht mehr den ganzen
  Lauf ab, Fehlversuche werden gezählt (pipeline_progress.translateFailures)
- translatePair als wiederverwendbarer Helfer extrahiert
- POST /pipeline/picture/:id/translate-fill: fehlende Übersetzungen
  (Sätze + Antwort-Wörter) eines Bildes nachholen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:03:11 +02:00
parent fb93d2296e
commit 985119bb03
3 changed files with 84 additions and 40 deletions

View File

@@ -57,20 +57,40 @@ function detokenize(translated, tokens, labelsFromClaude) {
async function callClaude({ system, user, maxTokens = 2000 }) {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) { const e = new Error('ANTHROPIC_API_KEY nicht konfiguriert'); e.status = 500; throw e; }
const res = await fetch(ANTHROPIC_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
body: JSON.stringify({
model: TRANSLATE_MODEL, max_tokens: maxTokens, system,
messages: [{ role: 'user', content: user }],
}),
});
if (!res.ok) { const err = await res.json().catch(() => ({})); const e = new Error(err.error?.message || `Claude API ${res.status}`); e.status = res.status; throw e; }
const data = await res.json();
let raw = data.content[0].text.trim();
const md = raw.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
if (md) raw = md[1];
return JSON.parse(raw);
// Retry bei Überlast/Rate-Limit/Netzfehler — der große Pipeline-Durchlauf macht
// viele Calls hintereinander; ohne Retry brach früher eine Sprache komplett weg.
const RETRYABLE = new Set([429, 500, 503, 529]);
const delays = [1000, 4000, 12000];
let lastErr;
for (let attempt = 0; attempt <= delays.length; attempt++) {
try {
const res = await fetch(ANTHROPIC_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
body: JSON.stringify({
model: TRANSLATE_MODEL, max_tokens: maxTokens, system,
messages: [{ role: 'user', content: user }],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
const e = new Error(err.error?.message || `Claude API ${res.status}`); e.status = res.status; throw e;
}
const data = await res.json();
let raw = data.content[0].text.trim();
const md = raw.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
if (md) raw = md[1];
return JSON.parse(raw);
} catch (err) {
lastErr = err;
// Netzfehler (kein status) oder retrybare HTTP-Codes → erneut versuchen
const retryable = err.status == null || RETRYABLE.has(err.status);
if (!retryable || attempt === delays.length) throw err;
await new Promise(r => setTimeout(r, delays[attempt]));
}
}
throw lastErr;
}
// Übersetzt einen Text inkl. Placeholder-Schutz.