feat: traduccion con contexto (sinopsis+tags) para titulos naturales de fansub
This commit is contained in:
@@ -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
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user