Spaces:
Sleeping
Sleeping
import os | |
import json | |
import platform | |
import locale | |
import logging | |
import tempfile | |
import torch | |
from transformers import MarianMTModel, MarianTokenizer | |
from langdetect import detect | |
import fitz # PyMuPDF | |
from reportlab.pdfgen import canvas | |
from reportlab.lib.pagesizes import A4 | |
from reportlab.pdfbase import pdfmetrics | |
from reportlab.pdfbase.ttfonts import TTFont | |
import gradio as gr | |
import numpy as np | |
# Configuración del logger | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Definición inicial de los modelos de traducción | |
MODELOS_TRADUCCION = { | |
'Inglés a Español': 'Helsinki-NLP/opus-mt-en-es', | |
'Español a Inglés': 'Helsinki-NLP/opus-mt-es-en', | |
'Inglés a Francés': 'Helsinki-NLP/opus-mt-en-fr', | |
'Francés a Inglés': 'Helsinki-NLP/opus-mt-fr-en', | |
'Inglés a Alemán': 'Helsinki-NLP/opus-mt-en-de', | |
'Alemán a Inglés': 'Helsinki-NLP/opus-mt-de-en', | |
'Inglés a Italiano': 'Helsinki-NLP/opus-mt-en-it', | |
'Italiano a Inglés': 'Helsinki-NLP/opus-mt-it-en', | |
'Inglés a Portugués': 'Helsinki-NLP/opus-mt-en-pt', | |
'Portugués a Inglés': 'Helsinki-NLP/opus-mt-pt-en', | |
} | |
# Mapeo de nombres completos de idiomas a códigos de idioma | |
LANGUAGE_MAP = { | |
'english': 'en', | |
'spanish': 'es', | |
'french': 'fr', | |
'german': 'de', | |
'italian': 'it', | |
'portuguese': 'pt', | |
# Agrega más idiomas según sea necesario | |
} | |
def detectar_idioma_sistema(): | |
""" | |
Detecta el idioma del sistema operativo utilizando locale. | |
Retorna el código del idioma (e.g., 'en', 'es'). | |
""" | |
try: | |
# Establecer la configuración regional para evitar DeprecationWarning | |
locale.setlocale(locale.LC_ALL, '') | |
idioma, _ = locale.getlocale() | |
if idioma: | |
idioma = idioma.split('_')[0] | |
idioma_lower = idioma.lower() | |
idioma_code = LANGUAGE_MAP.get(idioma_lower, 'es') # Predeterminado a 'es' si no se encuentra | |
else: | |
idioma_code = 'es' # Predeterminado a español si no se detecta | |
logger.info(f"Idioma del sistema detectado: {idioma_code}") | |
return idioma_code | |
except Exception as e: | |
logger.warning(f"No se pudo detectar el idioma del sistema: {e}") | |
return 'es' # Predeterminado a español en caso de error | |
def detectar_idioma_texto(texto): | |
""" | |
Detecta el idioma predominante del texto utilizando langdetect. | |
Retorna el código del idioma (e.g., 'en', 'es'). | |
""" | |
try: | |
idioma = detect(texto) | |
logger.info(f"Idioma detectado del texto: {idioma}") | |
return idioma | |
except Exception as e: | |
logger.error(f"Error al detectar el idioma: {e}") | |
return 'en' # Predeterminado a inglés si falla la detección | |
def actualizar_modelos_traduccion(idioma_origen, idioma_destino): | |
""" | |
Actualiza dinámicamente los modelos de traducción disponibles basado en el par de idiomas. | |
Retorna una tupla (clave, modelo_nombre) si existe el modelo, de lo contrario (None, None). | |
""" | |
mapa_idiomas = { | |
'en': 'Inglés', | |
'es': 'Español', | |
'fr': 'Francés', | |
'de': 'Alemán', | |
'it': 'Italiano', | |
'pt': 'Portugués', | |
# Agrega más idiomas según sea necesario | |
} | |
clave_origen = mapa_idiomas.get(idioma_origen, idioma_origen.capitalize()) | |
clave_destino = mapa_idiomas.get(idioma_destino, idioma_destino.capitalize()) | |
clave = f"{clave_origen} a {clave_destino}" | |
modelo = MODELOS_TRADUCCION.get(clave) | |
if modelo: | |
logger.info(f"Modelo de traducción encontrado para {clave}: {modelo}") | |
return clave, modelo | |
else: | |
logger.warning(f"No se encontró modelo de traducción para {clave}") | |
return None, None | |
def cargar_modelo_traduccion(origen, destino): | |
""" | |
Carga el modelo de traducción basado en los idiomas de origen y destino. | |
Retorna una tupla (tokenizer, model, dispositivo). | |
""" | |
clave, modelo_nombre = actualizar_modelos_traduccion(origen, destino) | |
if not modelo_nombre: | |
raise ValueError(f"No hay modelo de traducción disponible para {origen} a {destino}") | |
logger.info(f"Cargando el modelo de traducción: {clave}...") | |
tokenizer = MarianTokenizer.from_pretrained(modelo_nombre) | |
model = MarianMTModel.from_pretrained(modelo_nombre) | |
dispositivo = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
model.to(dispositivo) | |
logger.info(f"Modelo '{clave}' cargado en: {dispositivo}\n") | |
return tokenizer, model, dispositivo | |
def traducir_texto(tokenizer, model, textos, dispositivo, batch_size=8): | |
""" | |
Traduce una lista de textos utilizando el modelo y tokenizer proporcionados. | |
""" | |
traducciones = [] | |
for i in range(0, len(textos), batch_size): | |
batch = textos[i:i+batch_size] | |
inputs = tokenizer(batch, return_tensors="pt", padding=True, truncation=True) | |
inputs = {k: v.to(dispositivo) for k, v in inputs.items()} # Mover inputs al dispositivo | |
with torch.no_grad(): | |
traduccion = model.generate(**inputs) | |
traducciones += [tokenizer.decode(t, skip_special_tokens=True) for t in traduccion] | |
return traducciones | |
def obtener_rutas_fuentes(): | |
""" | |
Obtiene las rutas de las fuentes del sistema operativo. | |
""" | |
sistema = platform.system() | |
rutas_fuentes = [] | |
if sistema == 'Windows': | |
rutas_fuentes = [ | |
os.path.join(os.environ.get('WINDIR', 'C:\\Windows'), 'Fonts'), | |
os.path.expanduser('~\\AppData\\Local\\Microsoft\\Windows\\Fonts'), | |
os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Microsoft', 'Windows', 'Fonts') | |
] | |
elif sistema == 'Darwin': # macOS | |
rutas_fuentes = [ | |
'/System/Library/Fonts', | |
'/Library/Fonts', | |
os.path.expanduser('~/Library/Fonts') | |
] | |
elif sistema == 'Linux': | |
rutas_fuentes = [ | |
'/usr/share/fonts', | |
'/usr/local/share/fonts', | |
os.path.expanduser('~/.fonts') | |
] | |
else: | |
logger.warning(f"Sistema operativo no soportado: {sistema}") | |
return rutas_fuentes | |
def cachear_fuentes(): | |
""" | |
Cachea las fuentes disponibles en el sistema en un archivo JSON. | |
""" | |
rutas_fuentes = obtener_rutas_fuentes() | |
fuentes = {} | |
for ruta in rutas_fuentes: | |
if os.path.exists(ruta): | |
for root, dirs, files in os.walk(ruta): | |
for file in files: | |
if file.lower().endswith(('.ttf', '.otf')): | |
nombre_fuente = os.path.splitext(file)[0] | |
path_fuente = os.path.join(root, file) | |
# Evitar sobrescribir fuentes con el mismo nombre | |
if nombre_fuente not in fuentes: | |
fuentes[nombre_fuente] = path_fuente | |
cache_path = os.path.join(tempfile.gettempdir(), 'fuentes_sistema.json') | |
with open(cache_path, 'w', encoding='utf-8') as f: | |
json.dump(fuentes, f, ensure_ascii=False, indent=4) | |
logger.info(f"Fuentes cacheadas en: {cache_path}") | |
return fuentes | |
def cargar_fuentes_cache(): | |
""" | |
Carga las fuentes desde el caché o crea una nueva caché si no existe. | |
""" | |
cache_path = os.path.join(tempfile.gettempdir(), 'fuentes_sistema.json') | |
if not os.path.exists(cache_path): | |
logger.info("Cache de fuentes no encontrado. Creando cache...") | |
return cachear_fuentes() | |
with open(cache_path, 'r', encoding='utf-8') as f: | |
fuentes = json.load(f) | |
logger.info("Fuentes cargadas desde el cache.") | |
return fuentes | |
def registrar_fuentes(fuentes_sistema): | |
""" | |
Registra las fuentes disponibles en ReportLab. | |
Solo registra fuentes .ttf compatibles. | |
""" | |
fuentes_registradas = set(pdfmetrics.getRegisteredFontNames()) | |
for nombre, path in fuentes_sistema.items(): | |
# Verificar si el archivo es .ttf | |
if not path.lower().endswith('.ttf'): | |
logger.warning(f"Fuente {nombre} no es .ttf. Se omite su registro.") | |
continue | |
# Crear un nombre único para la fuente | |
nombre_registro = nombre | |
if nombre_registro not in fuentes_registradas: | |
try: | |
pdfmetrics.registerFont(TTFont(nombre_registro, path)) | |
fuentes_registradas.add(nombre_registro) | |
logger.info(f"Fuente registrada: {nombre_registro}") | |
except Exception as e: | |
logger.warning(f"No se pudo registrar la fuente {nombre}: {e}") | |
def buscar_fuente_similar(nombre_fuente_pdf, fuentes_sistema): | |
""" | |
Busca una fuente similar en las fuentes del sistema. | |
Si no encuentra una, retorna 'Helvetica'. | |
""" | |
nombre_fuente_pdf_lower = nombre_fuente_pdf.lower() | |
for nombre, path in fuentes_sistema.items(): | |
if nombre_fuente_pdf_lower in nombre.lower(): | |
return nombre # Retorna el nombre registrado en ReportLab | |
logger.warning(f"No se encontró una fuente similar para '{nombre_fuente_pdf}'. Usando 'Helvetica'.") | |
return "Helvetica" | |
def ajustar_tamano_fuente(texto, bbox, c, max_width, tamaño_fuente_original): | |
""" | |
Ajusta el tamaño de la fuente para que el texto se ajuste al ancho máximo. | |
""" | |
width_texto = c.stringWidth(texto, c._fontname, tamaño_fuente_original) | |
if width_texto > max_width: | |
nuevo_tamaño = tamaño_fuente_original * (max_width / width_texto) | |
return max(min(nuevo_tamaño, tamaño_fuente_original), 6) | |
return tamaño_fuente_original | |
def extraer_y_traducir_pdf(archivo_pdf, tokenizer, model, dispositivo, idioma_destino): | |
""" | |
Extrae el contenido del PDF, traduce el texto y crea un nuevo PDF traducido. | |
""" | |
documento = fitz.open(archivo_pdf.name) | |
pdf_traducido_path = os.path.splitext(archivo_pdf.name)[0] + f"_traducido_{idioma_destino}.pdf" | |
fuentes_sistema = cargar_fuentes_cache() | |
registrar_fuentes(fuentes_sistema) | |
# Crear un canvas ReportLab con el tamaño de la primera página | |
primera_pagina = documento.load_page(0) | |
rect = primera_pagina.rect | |
ancho, alto = rect.width, rect.height | |
c = canvas.Canvas(pdf_traducido_path, pagesize=(ancho, alto)) | |
textos = [] | |
posiciones = [] | |
# Extraer todos los textos y sus posiciones | |
for numero_pagina in range(len(documento)): | |
pagina = documento.load_page(numero_pagina) | |
bloques = pagina.get_text("dict")["blocks"] | |
for bloque in bloques: | |
if bloque['type'] == 0: # texto | |
for linea in bloque["lines"]: | |
for span in linea["spans"]: | |
textos.append(span["text"]) | |
posiciones.append((span["bbox"], span["font"], span["size"], numero_pagina)) | |
# Traducir texto | |
traducciones = traducir_texto(tokenizer, model, textos, dispositivo) | |
# Dibujar el texto traducido | |
idx_texto = 0 | |
total_paginas = len(documento) | |
for numero_pagina in range(total_paginas): | |
pagina = documento.load_page(numero_pagina) | |
rect = pagina.rect | |
ancho, alto = rect.width, rect.height | |
# Ajustar el tamaño de página al tamaño original del PDF | |
c.setPageSize((ancho, alto)) | |
# Definir márgenes dinámicos en base al tamaño de la página, ej: 5% de ancho y alto | |
margen_x = ancho * 0.05 | |
margen_y = alto * 0.05 | |
# Procesar texto de esta página | |
pagina_bloques = pagina.get_text("dict")["blocks"] | |
for bloque in pagina_bloques: | |
if bloque['type'] == 0: | |
for linea in bloque["lines"]: | |
for span in linea["spans"]: | |
# Obtener el texto traducido correspondiente | |
texto_traducido = traducciones[idx_texto] | |
bbox, font, size, span_pagina = posiciones[idx_texto] | |
idx_texto += 1 | |
x0, y0, x1, y1 = bbox | |
# Ajustar coordenadas al sistema de ReportLab (y invertida) | |
x = x0 | |
y = alto - y1 | |
# Buscar fuente similar | |
fuente_encontrada = buscar_fuente_similar(font, fuentes_sistema) | |
# Intentar establecer la fuente encontrada | |
try: | |
c.setFont(fuente_encontrada, size) | |
except: | |
logger.warning(f"Fuente '{fuente_encontrada}' no registrada. Usando 'Helvetica'.") | |
fuente_encontrada = "Helvetica" | |
c.setFont(fuente_encontrada, size) | |
# Ajustar el tamaño del texto si excede el ancho disponible | |
max_width = (x1 - x0) - margen_x if (x1 - x0) > 0 else (ancho - 2 * margen_x) | |
nuevo_tamaño = ajustar_tamano_fuente(texto_traducido, bbox, c, max_width, size) | |
# Establecer el nuevo tamaño de fuente | |
try: | |
c.setFont(fuente_encontrada, nuevo_tamaño) | |
except: | |
logger.warning(f"No se pudo ajustar el tamaño de la fuente para '{fuente_encontrada}'. Usando 'Helvetica'.") | |
fuente_encontrada = "Helvetica" | |
c.setFont(fuente_encontrada, nuevo_tamaño) | |
# Dibujar texto | |
try: | |
c.drawString(x, y, texto_traducido) | |
except Exception as e: | |
logger.error(f"Error al dibujar texto: {e}") | |
# Intentar con Helvetica por defecto | |
c.setFont("Helvetica", nuevo_tamaño) | |
c.drawString(x, y, texto_traducido) | |
# Procesar imágenes | |
imagenes = [b for b in pagina_bloques if b['type'] == 1] | |
for imagen in imagenes: | |
if 'xref' not in imagen: | |
continue | |
try: | |
x0, y0, x1, y1 = imagen["bbox"] | |
ancho_img, alto_img = x1 - x0, y1 - y0 | |
img = fitz.Pixmap(documento, imagen["xref"]) | |
if img.n > 4: | |
img = fitz.Pixmap(fitz.csRGB, img) | |
imagen_path = os.path.join(tempfile.gettempdir(), f"imagen_{numero_pagina}.png") | |
img.save(imagen_path) | |
img.close() | |
c.drawImage(imagen_path, x0, alto - y1 - alto_img, width=ancho_img, height=alto_img) | |
except Exception as e: | |
logger.error(f"Error al procesar imagen: {e}") | |
continue | |
c.showPage() | |
c.save() | |
return pdf_traducido_path | |
def pdf_preview(file): | |
""" | |
Previsualiza la primera página del PDF como una imagen. | |
""" | |
try: | |
doc = fitz.open(file.name) | |
page = doc[0] | |
pix = page.get_pixmap() | |
image = np.frombuffer(pix.samples, np.uint8).reshape(pix.height, pix.width, pix.n) | |
if pix.n == 4: | |
image = image[:, :, :3] | |
return image | |
except Exception as e: | |
logger.error(f"Error al previsualizar el PDF: {e}") | |
return None | |
def boton_actualizar_fuentes(files): | |
""" | |
Actualiza las fuentes del sistema subiendo nuevas fuentes. | |
""" | |
try: | |
if files: | |
fuentes_cache = cargar_fuentes_cache() | |
for file in files: | |
if file.name.lower().endswith('.ttf'): | |
destino = os.path.join(tempfile.gettempdir(), file.name) | |
with open(destino, 'wb') as f_dest: | |
f_dest.write(file.read()) | |
nombre_fuente = os.path.splitext(file.name)[0] | |
fuentes_cache[nombre_fuente] = destino | |
logger.info(f"Fuente '{file.name}' subida y guardada en {destino}") | |
else: | |
logger.warning(f"Archivo '{file.name}' no es una fuente .ttf y será omitido.") | |
# Actualizar el caché | |
cache_path = os.path.join(tempfile.gettempdir(), 'fuentes_sistema.json') | |
with open(cache_path, 'w', encoding='utf-8') as f: | |
json.dump(fuentes_cache, f, ensure_ascii=False, indent=4) | |
# Volver a registrar fuentes | |
registrar_fuentes(fuentes_cache) | |
else: | |
cachear_fuentes() | |
return "Fuentes actualizadas exitosamente." | |
except Exception as e: | |
logger.error(f"Error al actualizar fuentes: {e}") | |
return f"Error al actualizar fuentes: {e}" | |
def procesar_pdf(archivo_pdf, fuentes_subidas): | |
""" | |
Función principal para procesar y traducir el PDF. | |
""" | |
try: | |
if not archivo_pdf: | |
return None, "No se ha subido ningún archivo PDF." | |
# Extraer texto para detectar el idioma | |
documento = fitz.open(archivo_pdf.name) | |
texto_completo = "" | |
for pagina in documento: | |
texto_completo += pagina.get_text() | |
idioma_origen = detectar_idioma_texto(texto_completo) | |
idioma_sistema = detectar_idioma_sistema() | |
# Si el idioma de origen y destino son iguales, no realizar traducción | |
if idioma_origen == idioma_sistema: | |
logger.info("El idioma de origen y destino son iguales. No se realizará la traducción.") | |
return archivo_pdf.name, "El idioma de origen y destino son iguales. No se realizó la traducción." | |
# Cargar el modelo de traducción automáticamente | |
tokenizer, model, dispositivo = cargar_modelo_traduccion(idioma_origen, idioma_sistema) | |
# Traducir el PDF | |
pdf_traducido_path = extraer_y_traducir_pdf(archivo_pdf, tokenizer, model, dispositivo, idioma_sistema) | |
return pdf_traducido_path, "Traducción completada exitosamente." | |
except Exception as e: | |
logger.error(f"Error en procesar_pdf: {e}") | |
return None, f"Error en la traducción: {e}" | |
def actualizar_fuentes_cache(): | |
""" | |
Función para actualizar el caché de fuentes. | |
""" | |
try: | |
cachear_fuentes() | |
return "Fuentes cacheadas exitosamente." | |
except Exception as e: | |
logger.error(f"Error al cachear fuentes: {e}") | |
return f"Error al cachear fuentes: {e}" | |
# Interfaz de usuario con Gradio | |
with gr.Blocks( | |
title="Traductor de PDF Multilenguaje", | |
theme=gr.themes.Default( | |
primary_hue="blue", spacing_size="md", radius_size="lg" | |
) | |
) as iface: | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("# Traductor de PDF Multilenguaje") | |
pdf_input = gr.File(label="Sube tu PDF", file_types=['.pdf']) | |
# Eliminamos el Dropdown de selección manual del modelo de traducción | |
fuentes_subidas = gr.File(label="Sube fuentes faltantes (opcional)", file_count="multiple", file_types=['.ttf']) | |
actualizar_fuentes_btn = gr.Button("Actualizar Fuentes del Sistema") | |
actualizar_fuentes_output = gr.Textbox(label="Actualización de Fuentes", interactive=False) | |
actualizar_fuentes_btn.click( | |
fn=boton_actualizar_fuentes, | |
inputs=fuentes_subidas, | |
outputs=actualizar_fuentes_output | |
) | |
with gr.Column(scale=1): | |
gr.Markdown("## Vista Previa") | |
preview = gr.Image(label="Vista Previa", visible=True) | |
traducir_btn = gr.Button("Traducir PDF") | |
estado_traduccion = gr.Textbox(label="Estado", interactive=False) | |
traducir_btn.click( | |
fn=procesar_pdf, | |
inputs=[pdf_input, fuentes_subidas], | |
outputs=[gr.File(label="Descargar PDF traducido"), estado_traduccion] | |
) | |
# Vista previa del PDF | |
pdf_input.change( | |
fn=pdf_preview, | |
inputs=pdf_input, | |
outputs=preview | |
) | |
# Ejecutar la interfaz de usuario con la opción de compartir públicamente | |
iface.launch(share=True) | |