Todos los que amamos la lectura nos hemos topado con ese muro: un libro o una revista técnica que nos morimos por leer, pero que solo está disponible en inglés. Sí, existen traductores automáticos, pero suelen ser fríos, pierden las sutilezas y, lo peor de todo, destrozan el formato del libro original.
Aprovechando la potencia de mi nuevo Mac Studio M4 Max, decidí que ya era hora de dejar de pelearme con diccionarios y crear una herramienta que hiciera el trabajo sucio por mí, pero con la calidad de un traductor que entiende el contexto.
La idea: No queremos palabras, queremos historias
La premisa era sencilla: quería un proceso que tomara un archivo EPUB, lo tradujera usando un modelo de lenguaje (LLM) potente como Command R 35B y me devolviera otro EPUB idéntico en formato pero en mi idioma.
El camino no fue lineal, y como suele pasar en el desarrollo, la primera idea no fue la definitiva.
Paso 1: El desvío por el Markdown
Mi idea original era convertir el libro a Markdown, traducirlo y volver a generar el EPUB. ¿El problema? Los libros electrónicos son estructuras complejas de XHTML y CSS. Al convertir a Markdown, perdía los estilos específicos de la editorial, las fuentes embebidas y esa maquetación que hace que un libro sea cómodo de leer.
Paso 2: La cirugía estética (XHTML bloque a bloque)
Cambiamos de estrategia. Empezamos a abrir las «tripas» del libro y a extraer cada párrafo del código XHTML para enviárselo al modelo. Funcionaba, pero surgió un enemigo inesperado: la pérdida de contexto.
Si le envías a una IA diez párrafos sueltos, no sabe si «the ship» es una nave espacial o un barco, o si el protagonista es hombre o mujer si no se menciona en ese trozo exacto. La traducción perdía coherencia. Además, el modelo a veces se «cansaba» y dejaba los últimos párrafos de cada bloque en inglés.
Paso 3: El equilibrio perfecto (Markdown interno)
La solución final fue un enfoque híbrido. En lugar de convertir el libro entero, el script ahora:
- Extrae el contenido de cada capítulo.
- Lo limpia de «ruido» de código convirtiéndolo temporalmente a Markdown.
- Envía el capítulo entero al modelo. Al ver el capítulo completo, la IA entiende el tono, el género y la continuidad narrativa.
- El resultado se convierte de nuevo a HTML y se inyecta en la estructura original del libro.
El toque final: Un ciudadano ejemplar en macOS
Para que el proceso fuera perfecto, añadimos dos detalles de «calidad de vida»:
- Separación silábica: El script marca el nuevo libro como «Español». Esto parece una tontería, pero si no lo haces, el Kindle o el Kobo intentarán separar las palabras siguiendo reglas inglesas, y verás cortes extrañísimos al final de cada línea.
- Gestión de memoria: Usando la línea de comandos de LM Studio, el script carga el modelo al empezar y lo descarga al terminar. Así, mis 36GB de RAM vuelven a estar disponibles para programar en C# o C++ en cuanto el libro está listo.
¿Piratería o cultura?
Esta herramienta no es para piratear. Es para acceder a cultura que, de otro modo, se me quedaría fuera del alcance por la barrera idiomática. Es usar la tecnología para lo que mejor sabe hacer: derribar muros.
Si tienes un Mac potente o una buena GPU y quieres probarlo, aquí te dejo el script final. Solo necesitas LM Studio corriendo de fondo.
El Script: EPUB Translator v4.3
Python
import os
import sys
import subprocess
import warnings
import shutil
import ebooklib
from ebooklib import epub
from bs4 import BeautifulSoup, XMLParsedAsHTMLWarning
from markdownify import markdownify as md
import markdown
from openai import OpenAI
# --- CONFIGURACIÓN ---
PORT = "46357"
MODEL_ID = "c4ai-command-r-08-2024"
TIMEOUT_LLM = 1200.0 # 20 minutos por capítulo
DEBUG_ROOT = "debug_traduccion"
ORIGINAL_DIR = os.path.join(DEBUG_ROOT, "1_original_xhtml")
TRANSLATED_DIR = os.path.join(DEBUG_ROOT, "2_traducido_xhtml")
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
BASE_URL = f"http://localhost:{PORT}/v1"
client = OpenAI(base_url=BASE_URL, api_key="lm-studio", timeout=TIMEOUT_LLM)
def preparar_carpetas():
if os.path.exists(DEBUG_ROOT): shutil.rmtree(DEBUG_ROOT)
os.makedirs(ORIGINAL_DIR); os.makedirs(TRANSLATED_DIR)
def manage_model(action="load"):
"""Gestiona el modelo usando el CLI 'lms' de LM Studio."""
try:
if action == "load":
print(f"🚀 Cargando modelo: {MODEL_ID}...")
subprocess.run(["lms", "load", MODEL_ID], check=True, capture_output=True)
else:
print("💤 Descargando modelos...")
subprocess.run(["lms", "unload", "--all"], check=True, capture_output=True)
print(f"✅ Operación '{action}' completada.")
except Exception as e:
print(f"⚠️ Error al gestionar modelo: {e}")
def is_untranslated(original, translated):
"""Detecta si el modelo ha devuelto el texto original sin cambios."""
def clean(t): return "".join(t.split()).lower()
return clean(original) == clean(translated)
def translate_markdown(content, source_lang, genre):
system_prompt = f"""You are a professional literary translator.
Translate this Markdown from {source_lang} to Spanish. Genre: {genre}.
RULES:
1. Preserve all Markdown formatting.
2. Return ONLY the translated Markdown."""
try:
response = client.chat.completions.create(
model="local-model",
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": content}],
temperature=0.1
)
translated = response.choices[0].message.content
if is_untranslated(content, translated):
print(" [!] El modelo devolvió el texto original. Reintentando...")
return None
return translated
except Exception as e:
print(f" [!] Error API: {e}")
return None
def process_epub(input_path, source_lang, genre):
preparar_carpetas()
manage_model("load")
book = epub.read_epub(input_path)
# Forzar metadatos de idioma a español para hyphenation correcta
book.metadata['http://purl.org/dc/elements/1.1/']['language'] = [('es', {})]
book.set_language('es')
for item in book.get_items():
if item.get_type() == ebooklib.ITEM_DOCUMENT:
item_name = item.get_name()
# Saltamos portadas e índices para no romper enlaces estructurales
skip_keywords = ["TOC", "COVER", "TITLEPAGE", "NAV", "PORTADA"]
if any(key in item_name.upper() for key in skip_keywords):
print(f"⏩ Saltando: {item_name}")
continue
print(f"📖 Traduciendo: {item_name}")
file_name = item_name.replace("/", "_")
original_xhtml = item.get_content().decode('utf-8')
# Guardar original para debug
with open(os.path.join(ORIGINAL_DIR, file_name), "w", encoding="utf-8") as f:
f.write(original_xhtml)
# Paso a Markdown -> Traducción -> Paso a HTML
md_content = md(original_xhtml, heading_style="ATX")
translated_md = translate_markdown(md_content, source_lang, genre)
if translated_md:
new_body = markdown.markdown(translated_md)
soup = BeautifulSoup(original_xhtml, features="xml")
if soup.body:
soup.body.clear()
soup.body.append(BeautifulSoup(new_body, "html.parser"))
# Marcar el archivo interno también como español
html_tag = soup.find('html')
if html_tag:
html_tag['lang'] = 'es'
html_tag['xml:lang'] = 'es'
final_xhtml = soup.encode('utf-8')
else:
final_xhtml = original_xhtml.encode('utf-8')
item.set_content(final_xhtml)
with open(os.path.join(TRANSLATED_DIR, file_name), "wb") as f:
f.write(final_xhtml)
out_name = f"ES_{os.path.basename(input_path)}"
epub.write_epub(out_name, book)
manage_model("unload")
print(f"\n✅ Proceso finalizado: {out_name}")
if __name__ == "__main__":
if len(sys.argv) < 4:
print("Uso: python traductor.py libro.epub English ScienceFiction")
else:
process_epub(sys.argv[1], sys.argv[2], sys.argv[3])
Entrada creada con el apoyo de Gemini 3.1