fix: traduccion con rotacion de modelos + filtro anti-basura + benchmark de modelos

This commit is contained in:
Renato
2026-08-05 05:57:55 +08:00
parent 13e507fbb9
commit 636d629034
3 changed files with 159 additions and 40 deletions
+19
View File
@@ -0,0 +1,19 @@
const Database = require("better-sqlite3");
const db = new Database("./data/worst-scan.db");
// Limpiar traducciones basura (las que repiten el prompt)
const junkPrefixes = ["The user wants", "The task", "I need to", "claro que", "esta es una traducción", "aquí tienes", "la traducción es", "por supuesto", "The translation", "Here is", "The title", "Sure", "Voy a traducir", "The user asked"];
const rows = db.prepare("SELECT id, title, title_es FROM posts WHERE title_es IS NOT NULL AND title_es != ''").all();
let cleaned = 0;
for (const r of rows) {
const es = String(r.title_es || "");
const isJunk = junkPrefixes.some((j) => es.startsWith(j)) || es.length > 120;
if (isJunk) {
db.prepare("UPDATE posts SET title_es = NULL WHERE id = ?").run(r.id);
console.log(`limpiado: "${String(r.title).slice(0, 40)}" -> era basura: "${es.slice(0, 40)}"`);
cleaned++;
}
}
console.log(`\nLimpiadas ${cleaned} traducciones basura`);
console.log("Quedan limpias:", db.prepare("SELECT COUNT(*) c FROM posts WHERE title_es IS NOT NULL AND title_es != ''").get().c);
db.close();
+100 -37
View File
@@ -1,13 +1,12 @@
// 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
// CON ROTACIÓN DE MODELOS: usa TODOS los modelos free del proxy y salta
// los que dan 429/vacío (rate limit). Con reintentos y progreso.
// Corre standalone: node translate_existing.js [LIMIT]
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
try { db.exec("ALTER TABLE posts ADD COLUMN title_es TEXT"); } catch {}
const PROXY_URL = "http://127.0.0.1:6446/v1";
const PROXY_KEY = "tw0rxo314kvga9lfbp2nj5sde7hiq6uc";
@@ -24,6 +23,31 @@ Normas:
- Si hay números de capítulo/volumen/año, mantenelos.
- Respondé en UNA línea.`;
// Modelos en orden de preferencia (benchmark real 2026-08):
// nemotron-3-nano-omni y openrouter/free traducen BIEN (no meta-pensamiento).
// Los demas son fallbacks que a veces dan basura (el filtro la descarta).
const MODEL_CANDIDATES = [
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", // ⭐ traduce bien, 5.5s
"openrouter/free", // ⭐ traduce bien, 2.7s
"deepseek-v4-flash-free", // bueno cuando no esta cageado
"big-pickle",
"tencent/hy3:free",
"nvidia/nemotron-3-ultra-550b-a55b:free",
"mimo-v2.5-free",
"cohere/north-mini-code:free",
"inclusionai/ling-3.0-flash:free",
"nvidia/nemotron-3-super-120b-a12b:free",
"nemotron-3-ultra-free",
"north-mini-code-free",
"poolside/laguna-s-2.1:free",
"poolside/laguna-xs-2.1:free",
"kilo-auto/free",
"forever_free",
];
// Modelos que fallaron (rate limit / vacío) -> no reintentar por un rato
const deadModels = new Map(); // modelo -> timestamp cuando se puede reintentar
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 "];
@@ -32,63 +56,102 @@ function isProbablySpanish(t) {
return count >= 3;
}
async function tryModel(model, title) {
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: title },
],
max_tokens: 200,
temperature: 0.3,
}),
signal: AbortSignal.timeout(60000),
});
if (!res.ok) return { ok: false, rateLimited: res.status === 429 };
const data = await res.json();
const content = data?.choices?.[0]?.message?.content;
const text = (content || "").trim().replace(/^["']|["']$/g, "");
if (!text) return { ok: false, rateLimited: true }; // vacío = agotado
// Rechazar respuestas que NO son una traducción (repiten el prompt o razonan).
if (!isValidTranslation(text)) return { ok: false, rateLimited: false };
return { ok: true, text };
}
// Filtra respuestas basura: si el modelo repite el prompt o razona en voz alta
// ("The user wants...", "I need to translate...", "Esta es una traducción...")
// no es una traducción válida.
function isValidTranslation(text) {
const t = text.toLowerCase();
const junk = [
"the user wants", "the task", "i need to", "i'll", "the manga title",
"esta es una traducción", "aquí tienes", "la traducción es", "claro que",
"por supuesto", "the translation", "here is", "the title", "sure",
"voy a traducir", "the user asked", "the user is",
];
// Si es muy larga (> 120 chars) probablemente es razonamiento, no título.
if (text.length > 120) return false;
for (const j of junk) if (t.includes(j)) return false;
return true;
}
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;
// Bucle sobre modelos con backtracking: si todos fallan, reintentar tras pausa
for (let attempt = 0; attempt < 3; attempt++) {
for (const model of MODEL_CANDIDATES) {
const retryAt = deadModels.get(model) || 0;
if (Date.now() < retryAt) continue;
try {
const r = await tryModel(model, clean);
if (r.ok) return r.text;
if (r.rateLimited) {
// Marcar como muerto por 5 min
deadModels.set(model, Date.now() + 5 * 60 * 1000);
console.log(` [rate-limit] ${model} -> pausado 5 min`);
}
} catch {
deadModels.set(model, Date.now() + 60 * 1000);
}
}
// Todos fallaron: esperar 30s antes del siguiente intento
if (attempt < 2) {
await new Promise((r) => setTimeout(r, 30000));
}
}
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();
const limit = parseInt(process.argv[2] || "0", 10);
const posts = db.prepare("SELECT id, gid, title, title_es FROM posts").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})` : ""}`);
console.log(`Total: ${posts.length} | sin traducción: ${toTranslate.length}${limit ? ` (lote ${limit})` : ""}`);
let ok = 0, fail = 0, skip = 0;
let ok = 0, fail = 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)}`);
console.log(`[${i + 1}/${toTranslate.length}] ✓ ${String(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)`);
console.log(`[${i + 1}/${toTranslate.length}] ✗ ${String(p.title).slice(0, 40)}`);
}
// Delay pequeño entre requests para no saturar el proxy
await new Promise((r) => setTimeout(r, 300));
// Pausa corta entre títulos para no saturar
await new Promise((r) => setTimeout(r, 1000));
}
console.log(`\n=== RESULTADO: ${ok} traducidos, ${fail} fallaron, ${skip} omitidos ===`);
console.log(`\n=== RESULTADO: ${ok} traducidos, ${fail} fallaron ===`);
db.close();
}