feat: traduccion con contexto (sinopsis+tags) para titulos naturales de fansub

This commit is contained in:
Renato
2026-08-05 07:13:07 +08:00
parent 91ffc714f3
commit c50deadee8
2 changed files with 39 additions and 28 deletions
+30 -17
View File
@@ -11,17 +11,27 @@ 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";
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.`;
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 JUNTO CON su sinopsis y tags como contexto.
Tu trabajo es formular un título NATURAL en español para ese manga, como lo titularía una editorial real.
REGLAS:
- No traduzcas el título literalmente. Usá el contexto (tags + sinopsis) para entender DE QUÉ TRATA el manga y titulalo en español de forma natural y atractiva.
- Si el título original ya es claro en español, adaptalo levemente. Si es confuso, reformulalo basándote en la sinopsis.
- 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.`;
// Arma el mensaje de usuario con título + contexto (tags + sinopsis).
function buildUserPrompt(post) {
let tags = post.tags || [];
if (typeof tags === "string") { try { tags = JSON.parse(tags); } catch { tags = []; } }
const lines = [`Título original: ${post.title}`];
if (Array.isArray(tags) && tags.length) lines.push(`Tags: ${tags.slice(0, 25).join(", ")}`);
if (post.summary && post.summary.length > 20) lines.push(`Sinopsis: ${post.summary.slice(0, 600)}`);
return lines.join("\n");
}
// Modelos en orden de preferencia (benchmark + tests reales 2026-08):
// nemotron-3-ultra-550b y nemotron-3-nano-omni traducen BIEN con max_tokens>=600.
@@ -98,10 +108,11 @@ function isValidTranslation(text) {
return true;
}
async function translate(title) {
const clean = (title || "").trim();
async function translate(post) {
const clean = (post.title || "").trim();
if (!clean) return null;
if (isProbablySpanish(clean)) return clean;
const userMsg = buildUserPrompt(post);
// Bucle sobre modelos con backtracking: si todos fallan, reintentar tras pausa
for (let attempt = 0; attempt < 3; attempt++) {
@@ -109,7 +120,7 @@ async function translate(title) {
const retryAt = deadModels.get(model) || 0;
if (Date.now() < retryAt) continue;
try {
const r = await tryModel(model, clean);
const r = await tryModel(model, userMsg);
if (r.ok) return r.text;
if (r.rateLimited) {
// Marcar como muerto por 5 min
@@ -130,15 +141,17 @@ async function translate(title) {
async function main() {
const limit = parseInt(process.argv[2] || "0", 10);
const posts = db.prepare("SELECT id, gid, title, title_es FROM posts").all();
const retake = process.env.RETAKE === "1";
const posts = db.prepare("SELECT id, gid, title, title_es, tags, summary FROM posts").all();
let toTranslate = posts.filter(p => !p.title_es);
if (retake) toTranslate = posts.filter(p => p.title_es); // re-traducir los existentes
if (limit > 0) toTranslate = toTranslate.slice(0, limit);
console.log(`Total: ${posts.length} | sin traducción: ${toTranslate.length}${limit ? ` (lote ${limit})` : ""}`);
console.log(`Total: ${posts.length} | a procesar: ${toTranslate.length}${retake ? " (RETAKE: re-traduciendo existentes)" : ""}${limit ? ` (lote ${limit})` : ""}`);
let ok = 0, fail = 0;
for (let i = 0; i < toTranslate.length; i++) {
const p = toTranslate[i];
const t = await translate(p.title);
const t = await translate(p);
if (t) {
db.prepare("UPDATE posts SET title_es = ?, updated_at = datetime('now') WHERE id = ?").run(t, p.id);
ok++;
+9 -11
View File
@@ -7,17 +7,15 @@ import "server-only"
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.`
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> {