feat: traduccion de titulos al espanol (title_es) + card limpia (fecha en vez de tags/min) + poller traduce nuevos
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user