feat: traduccion de titulos al espanol (title_es) + card limpia (fecha en vez de tags/min) + poller traduce nuevos

This commit is contained in:
Renato
2026-08-05 05:35:41 +08:00
parent fcb418101e
commit d3cd79f50d
10 changed files with 235 additions and 38 deletions
+74
View File
@@ -0,0 +1,74 @@
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 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.`
// 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
const models = ["deepseek-v4-flash-free", "big-pickle"]
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: 200,
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
if (!content || !content.trim()) continue
return content.trim().replace(/^["']|["']$/g, "")
} catch {
continue
}
}
return null
}
// 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
}