Files
worst-scan-web/src/lib/translate.ts
T

110 lines
4.6 KiB
TypeScript

import "server-only"
// Traducción de títulos de manga al español usando el proxy free-ide local.
// Prefiere deepseek-v4-flash-free (probado, devuelve contenido limpio) con
// fallback a big-pickle. No depende de ninguna API externa de pago.
const PROXY_URL = process.env.FREE_IDE_PROXY_URL || "http://127.0.0.1:6446/v1"
const PROXY_KEY = process.env.FREE_IDE_PROXY_KEY || "tw0rxo314kvga9lfbp2nj5sde7hiq6uc"
const SYSTEM_PROMPT = `Sos el editor de títulos de un fansub de manga que traduce del japonés/inglés al español latinoamericano.
Recibís el título de un manga/doujinshi. Tu trabajo es ADAPTARLO a un título natural en español, como lo titularía una editorial de manga real.
REGLAS:
- NO traduzcas palabra por palabra. Reescribí el título para que suene natural y atractivo en español, manteniendo el significado.
- Mantené nombres propios (personajes, artistas, marcas, franquicias) sin traducir.
- Mantené números de capítulo/volumen/año.
- Si hay un término japonés de uso común en el fandom (ecchi, harem, netorare, yuri, etc.) podés dejarlo o traducirlo según suene mejor.
- El resultado debe leerse como un título de manga en español, no como una traducción de Google.
- Respondé SOLO con el título final, UNA línea, sin comillas, sin explicaciones, sin corchetes de autor.`
// Llama al proxy y devuelve el texto traducido (o null si falla).
export async function translateTitle(title: string): Promise<string | null> {
const clean = (title || "").trim()
if (!clean) return null
// Si ya parece estar en español, no traducir (heurística ligera).
if (isProbablySpanish(clean)) return clean
// Modelos en orden de preferencia (benchmark + tests reales 2026-08):
// nemotron-3-ultra-550b traduce bien con max_tokens>=600.
const models = [
"nvidia/nemotron-3-ultra-550b-a55b:free",
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
"nemotron-3-ultra-free",
"openrouter/free",
"deepseek-v4-flash-free",
"big-pickle",
"tencent/hy3: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",
]
for (const model of models) {
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: 600, // >=500: los reasoning models gastan los primeros tokens en razonamiento
temperature: 0.3,
}),
signal: AbortSignal.timeout(90000),
})
if (!res.ok) continue
const data = await res.json()
const content: string | undefined = data?.choices?.[0]?.message?.content
const text = (content || "").trim().replace(/^["']|["']$/g, "")
if (!text) continue
// Rechazar respuestas que NO son traducción (repiten el prompt).
if (!isValidTranslation(text)) continue
return text
} catch {
continue
}
}
return null
}
// Filtra respuestas basura: si el modelo razona en voz alta ("The user
// wants...") o entrega explicaciones, no es una traducción válida.
function isValidTranslation(text: string): boolean {
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",
]
if (text.length > 120) return false
for (const j of junk) if (t.includes(j)) return false
return true
}
// Heurística: detecta si el título ya está mayormente en español (para no
// re-traducir nombres propios o títulos ya en español).
function isProbablySpanish(t: string): boolean {
// Si tiene caracteres CJK (japonés/chino) claramente no está en español.
if (/[\u3040-\u30ff\u4e00-\u9fff]/.test(t)) return false
// Palabras "muy en español" que indican que no necesita traducción.
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++
// Si tiene 3+ marcadores de español, asumimos que ya está en español.
return count >= 3
}