/** * Kör om de receptöversättningar som föll på den deterministiska verifieringen * (verification.ok = false) och därför ligger kvar som svenska. Skriver ut vilka * + varför, och köar TRANSLATE_RECIPE på nytt. Workern översätter om (upsert med * ny verifiering) → kör sedan publish-translations.ts all för att publicera de * som nu klarar. Idempotent: kör om tills listan är tom. * * pnpm --filter @app/worker exec tsx scripts/retry-failed-translations.ts # kör om * pnpm --filter @app/worker exec tsx scripts/retry-failed-translations.ts --dry-run # visa bara * * Fönster C (workern) måste vara igång med riktig AI-nyckel. * Kräver DATABASE_URL + REDIS_URL (laddas från .env). */ import { config as loadDotenv } from "dotenv"; import { existsSync } from "node:fs"; import path from "node:path"; import IORedis from "ioredis"; import { Queue } from "bullmq"; import { and, eq, sql } from "drizzle-orm"; import { createDatabase, schema } from "@app/database"; import { JOB_QUEUE_NAME } from "@app/shared-types"; for (const candidate of [".env", "../.env", "../../.env"]) { const p = path.resolve(process.cwd(), candidate); if (existsSync(p)) { loadDotenv({ path: p }); break; } } async function main() { if (!process.env.DATABASE_URL) throw new Error("Sätt DATABASE_URL."); const dryRun = process.argv.includes("--dry-run"); const { db, pool } = createDatabase(process.env.DATABASE_URL); const failed = await db .select({ recipeId: schema.recipeTranslations.recipeId, lang: schema.recipeTranslations.languageTag, title: schema.recipes.titleSv, verification: schema.recipeTranslations.verification, }) .from(schema.recipeTranslations) .innerJoin(schema.recipes, eq(schema.recipes.id, schema.recipeTranslations.recipeId)) .where( and( eq(schema.recipeTranslations.status, "draft_ai"), sql`${schema.recipeTranslations.verification}->>'ok' = 'false'`, ), ) .orderBy(schema.recipes.titleSv); if (failed.length === 0) { console.log("✓ Inga misslyckade översättningar – allt är verifierat och klart."); await pool.end(); return; } console.log(`${failed.length} översättning(ar) föll på verifieringen:\n`); for (const f of failed) { const notes = f.verification?.notes?.length ? f.verification.notes.join("; ") : "okänd orsak"; console.log(` [${f.lang}] ${f.title}`); console.log(` → ${notes}`); } if (dryRun) { console.log("\n(dry-run: köade inget. Kör utan --dry-run för att översätta om.)"); await pool.end(); return; } const connection = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", { maxRetriesPerRequest: null, }); const queue = new Queue(JOB_QUEUE_NAME, { connection }); await queue.addBulk( failed.map((f) => ({ name: "TRANSLATE_RECIPE", data: { jobType: "TRANSLATE_RECIPE", recipeId: f.recipeId, targetLanguageTag: f.lang }, })) as never, ); console.log(`\nKöade ${failed.length} omförsök. Workern (Fönster C) översätter om nu.`); console.log( "När kön tömts: pnpm --filter @app/worker exec tsx scripts/publish-translations.ts all", ); console.log("Kör det här scriptet igen för att se om något fortfarande faller (då kan det vara receptet självt som behöver en manuell titt)."); await queue.close(); await connection.quit(); await pool.end(); } main().then( () => process.exit(0), (e) => { console.error(e); process.exit(1); }, );