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_URL = "http://127.0.0.1:6446/v1";
const PROXY_KEY = "tw0rxo314kvga9lfbp2nj5sde7hiq6uc"; const PROXY_KEY = "tw0rxo314kvga9lfbp2nj5sde7hiq6uc";
const SYSTEM_PROMPT = `Sos un traductor de títulos de manga al español latinoamericano. 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 UN título de manga (en inglés, japonés romanizado, katakana, kanji o chino). Recibís el título de un manga/doujinshi JUNTO CON su sinopsis y tags como contexto.
Devolvés SOLO la traducción al español, sin comillas, sin corchetes de autor, Tu trabajo es formular un título NATURAL en español para ese manga, como lo titularía una editorial real.
sin explicaciones, sin "Traducción:" ni puntos finales. REGLAS:
Mantené el tono del título original. - 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.
Normas: - Si el título original ya es claro en español, adaptalo levemente. Si es confuso, reformulalo basándote en la sinopsis.
- No traduzcas nombres propios (personajes, artistas, marcas). - Mantené nombres propios (personajes, artistas, marcas, franquicias) sin traducir.
- Si el título ya está en español o es un nombre propio, devolvelo tal cual. - Mantené números de capítulo/volumen/año.
- Traducí términos como "Haha no Hi" -> "Día de la Madre". - 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.
- Si hay números de capítulo/volumen/año, mantenelos. - El resultado debe leerse como un título de manga en español, no como una traducción de Google.
- Respondé en UNA línea.`; - 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): // 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. // nemotron-3-ultra-550b y nemotron-3-nano-omni traducen BIEN con max_tokens>=600.
@@ -98,10 +108,11 @@ function isValidTranslation(text) {
return true; return true;
} }
async function translate(title) { async function translate(post) {
const clean = (title || "").trim(); const clean = (post.title || "").trim();
if (!clean) return null; if (!clean) return null;
if (isProbablySpanish(clean)) return clean; if (isProbablySpanish(clean)) return clean;
const userMsg = buildUserPrompt(post);
// Bucle sobre modelos con backtracking: si todos fallan, reintentar tras pausa // Bucle sobre modelos con backtracking: si todos fallan, reintentar tras pausa
for (let attempt = 0; attempt < 3; attempt++) { for (let attempt = 0; attempt < 3; attempt++) {
@@ -109,7 +120,7 @@ async function translate(title) {
const retryAt = deadModels.get(model) || 0; const retryAt = deadModels.get(model) || 0;
if (Date.now() < retryAt) continue; if (Date.now() < retryAt) continue;
try { try {
const r = await tryModel(model, clean); const r = await tryModel(model, userMsg);
if (r.ok) return r.text; if (r.ok) return r.text;
if (r.rateLimited) { if (r.rateLimited) {
// Marcar como muerto por 5 min // Marcar como muerto por 5 min
@@ -130,15 +141,17 @@ async function translate(title) {
async function main() { async function main() {
const limit = parseInt(process.argv[2] || "0", 10); 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); 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); 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; let ok = 0, fail = 0;
for (let i = 0; i < toTranslate.length; i++) { for (let i = 0; i < toTranslate.length; i++) {
const p = toTranslate[i]; const p = toTranslate[i];
const t = await translate(p.title); const t = await translate(p);
if (t) { if (t) {
db.prepare("UPDATE posts SET title_es = ?, updated_at = datetime('now') WHERE id = ?").run(t, p.id); db.prepare("UPDATE posts SET title_es = ?, updated_at = datetime('now') WHERE id = ?").run(t, p.id);
ok++; 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_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 PROXY_KEY = process.env.FREE_IDE_PROXY_KEY || "tw0rxo314kvga9lfbp2nj5sde7hiq6uc"
const SYSTEM_PROMPT = `Sos un traductor de títulos de manga al español latinoamericano. 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 UN título de manga (en inglés, japonés romanizado, katakana, kanji o chino). 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.
Devolvés SOLO la traducción al español, sin comillas, sin corchetes de autor, REGLAS:
sin explicaciones, sin "Traducción:" ni puntos finales. - NO traduzcas palabra por palabra. Reescribí el título para que suene natural y atractivo en español, manteniendo el significado.
Mantené el tono del título original. - Mantené nombres propios (personajes, artistas, marcas, franquicias) sin traducir.
Normas: - Mantené números de capítulo/volumen/año.
- No traduzcas nombres propios (personajes, artistas, marcas). - 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.
- Si el título ya está en español o es un nombre propio, devolvelo tal cual. - El resultado debe leerse como un título de manga en español, no como una traducción de Google.
- Traducí términos como "Haha no Hi" -> "Día de la Madre". - Respondé SOLO con el título final, UNA línea, sin comillas, sin explicaciones, sin corchetes de autor.`
- 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). // Llama al proxy y devuelve el texto traducido (o null si falla).
export async function translateTitle(title: string): Promise<string | null> { export async function translateTitle(title: string): Promise<string | null> {