// Traducción masiva de títulos existentes al español. // Usa el proxy free-ide local (deepseek-v4-flash-free con fallback). // Corre standalone: node translate_existing.js const Database = require("better-sqlite3"); const db = new Database("./data/worst-scan.db"); // Asegurar columna title_es (misma migracion que src/lib/db.ts) try { db.exec("ALTER TABLE posts ADD COLUMN title_es TEXT"); } catch {} // ya existe const PROXY_URL = "http://127.0.0.1:6446/v1"; const PROXY_KEY = "tw0rxo314kvga9lfbp2nj5sde7hiq6uc"; const SYSTEM_PROMPT = `Sos un traductor de títulos de manga al español latinoamericano. Recibís UN título de manga (en inglés, japonés romanizado, katakana, kanji o chino). Devolvés SOLO la traducción al español, sin comillas, sin corchetes de autor, sin explicaciones, sin "Traducción:" ni puntos finales. Mantené el tono del título original. Normas: - No traduzcas nombres propios (personajes, artistas, marcas). - Si el título ya está en español o es un nombre propio, devolvelo tal cual. - Traducí términos como "Haha no Hi" -> "Día de la Madre". - Si hay números de capítulo/volumen/año, mantenelos. - Respondé en UNA línea.`; function isProbablySpanish(t) { if (/[\u3040-\u30ff\u4e00-\u9fff]/.test(t)) return false; const spanishWords = ["el ", "la ", "los ", "las ", " de ", " y ", " un ", " una ", " para ", "con ", "por "]; let count = 0; for (const w of spanishWords) if (t.toLowerCase().includes(w)) count++; return count >= 3; } async function translate(title) { const clean = (title || "").trim(); if (!clean) return null; if (isProbablySpanish(clean)) return clean; for (const model of ["deepseek-v4-flash-free", "big-pickle"]) { try { const res = await fetch(`${PROXY_URL}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${PROXY_KEY}` }, body: JSON.stringify({ model, messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: clean }, ], max_tokens: 200, temperature: 0.3, }), signal: AbortSignal.timeout(90000), }); if (!res.ok) continue; const data = await res.json(); const content = data?.choices?.[0]?.message?.content; if (!content || !content.trim()) continue; return content.trim().replace(/^["']|["']$/g, ""); } catch { continue; } } return null; } async function main() { const limit = parseInt(process.env.LIMIT || "0", 10); const posts = db.prepare("SELECT id, gid, title, title_es FROM posts WHERE published=1 OR published=0").all(); let toTranslate = posts.filter(p => !p.title_es); if (limit > 0) toTranslate = toTranslate.slice(0, limit); console.log(`Total posts: ${posts.length} | sin traducción: ${toTranslate.length}${limit ? ` (probando ${limit})` : ""}`); let ok = 0, fail = 0, skip = 0; for (let i = 0; i < toTranslate.length; i++) { const p = toTranslate[i]; const t = await translate(p.title); if (t) { db.prepare("UPDATE posts SET title_es = ?, updated_at = datetime('now') WHERE id = ?").run(t, p.id); ok++; console.log(`[${i + 1}/${toTranslate.length}] ✓ ${p.title.slice(0, 40)} -> ${t.slice(0, 50)}`); } else { fail++; console.log(`[${i + 1}/${toTranslate.length}] ✗ ${p.title.slice(0, 40)} (sin traducción)`); } // Delay pequeño entre requests para no saturar el proxy await new Promise((r) => setTimeout(r, 300)); } console.log(`\n=== RESULTADO: ${ok} traducidos, ${fail} fallaron, ${skip} omitidos ===`); db.close(); } main().catch((e) => { console.error("ERROR:", e.message); process.exit(1); });