fix: Mejoras en generación de PDFs y resúmenes

- Corrige PDFGenerator para pasar contenido (no ruta)
- Agrega prompt siguiendo código.md (español, estructura académica)
- Limpia thinking tokens de respuesta AI
- Agrega skip de archivos ya procesados en watcher
- Implementa tablas LaTeX en PDFs (reportlab Table)
- Agrega load_dotenv() en main.py
- Actualiza .env con MiniMax config
- Agrega transcriptions/ a .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renato97
2026-02-25 17:12:00 +00:00
parent ee8fc183be
commit 1f6bfa771b
5 changed files with 207 additions and 11 deletions

View File

@@ -504,7 +504,7 @@ class ProcessManager:
# Notificar resumen completado
telegram_service.send_summary_complete(filepath.name, has_markdown=True)
# 4. Llamar a PDFGenerator.markdown_to_pdf()
# 4. Llam.markdown_to_pdfar a PDFGenerator()
pdf_path = None
try:
from services.pdf_generator import PDFGenerator
@@ -514,7 +514,9 @@ class ProcessManager:
pdf_generator = PDFGenerator()
pdf_path = md_path.with_suffix(".pdf")
pdf_generator.markdown_to_pdf(str(md_path), str(pdf_path))
# Leer el contenido markdown y pasarlo al generator
markdown_content = md_path.read_text(encoding="utf-8")
pdf_generator.markdown_to_pdf(markdown_content, pdf_path)
logger.info(
"PDF generado",

36
main.py
View File

@@ -5,10 +5,15 @@ CBFacil - Sistema de transcripción de audio con IA y Notion
Características:
- Polling de Nextcloud vía WebDAV
- Transcripción con Whisper (medium, GPU)
- Resúmenes con IA (GLM-4.7)
- Resúmenes con IA (MiniMax)
- Generación de PDF
- Notificaciones Telegram
"""
from dotenv import load_dotenv
# Cargar variables de entorno desde .env
load_dotenv()
import logging
import os
import sys
@@ -414,10 +419,31 @@ class PollingService:
# Extensiones de audio soportadas
audio_extensions = {".mp3", ".wav", ".m4a", ".mp4", ".webm", ".ogg", ".flac"}
pending_files = [
f for f in downloads_dir.iterdir()
if f.is_file() and f.suffix.lower() in audio_extensions and not f.name.startswith(".")
]
# Obtener transcripciones existentes para comparar
transcriptions_dir = settings.TRANSCRIPTIONS_DIR
processed_names = set()
if transcriptions_dir.exists():
for f in transcriptions_dir.iterdir():
if f.is_file() and f.suffix == ".txt":
# Extraer nombre base sin extensión
processed_names.add(f.stem)
# Filtrar solo archivos que NO han sido procesados
pending_files = []
for f in downloads_dir.iterdir():
if f.is_file() and f.suffix.lower() in audio_extensions and not f.name.startswith("."):
# Verificar si ya existe transcripción para este archivo
file_stem = f.stem
# También verificar versiones con " (copy)" o similar
if file_stem not in processed_names:
# Verificar variaciones del nombre
is_processed = False
for processed in processed_names:
if file_stem in processed or processed in file_stem:
is_processed = True
break
if not is_processed:
pending_files.append(f)
if not pending_files:
logger.debug("No pending audio files to process")

View File

@@ -69,8 +69,39 @@ class AISummaryService:
logger.debug("AISummaryService not configured, returning original text")
return text
default_prompt = "Resume el siguiente texto de manera clara y concisa:"
prompt = prompt_template.format(text=text) if prompt_template else f"{default_prompt}\n\n{text}"
# Prompt siguiendo código.md - resumen académico en español
default_prompt = """Eres un asistente académico especializado en crear resúmenes de estudio de alta calidad.
INSTRUCCIONES OBLIGATORIAS:
1. Escribe ÚNICAMENTE en español
2. El resumen debe seguir esta estructura:
- Título y objetivo de estudio
- Índice con 6-12 secciones
- Desarrollo conceptual (definiciones, mecanismos)
- Casos de aplicación (ejemplos concretos)
- Errores frecuentes
- Checklist de repaso
3. Cada concepto debe explicar: qué es, por qué importa, cómo se aplica
4. Evita listas sin explicación - siempre incluir el "por qué"
5. Para TABLAS usa formato LaTeX tabular:
\\begin{{tabular}}{{|c|l|l|}}
\\hline
Encabezado 1 & Encabezado 2 & Encabezado 3 \\\\
\\hline
dato1 & dato2 & dato3 \\\\
\\hline
\\end{{tabular}}
6. NO uses tablas ASCII ni markdown con | pipes
7. El resumen debe poder leerse en 15-25 minutos
8. NO incluyas rutas de archivos ni referencias técnicas
9. Sé conciso pero con densidad informativa útil para exámenes
Transcripción de clase:
{text}
Genera el resumen siguiendo las instrucciones arriba."""
prompt = prompt_template.format(text=text) if prompt_template else default_prompt.format(text=text)
payload = {
"model": self.model,
@@ -96,6 +127,29 @@ class AISummaryService:
result = response.json()
summary = result.get("choices", [{}])[0].get("message", {}).get("content", "")
# Limpiar respuesta: eliminar thinking tokens y ruido
# Buscar el primer encabezado markdown y cortar ahí
first_header = summary.find("\n# ")
if first_header == -1:
first_header = summary.find("# ")
if first_header > 0:
summary = summary[first_header:]
# Eliminar bloques de think/error si persisten
lines = summary.split("\n")
clean_lines = []
skip = False
for line in lines:
if line.strip().startswith("<think>") or line.strip().endswith("</think>"):
skip = True
continue
if skip and line.strip() and not line.startswith(" "):
skip = False
if not skip:
clean_lines.append(line)
summary = "\n".join(clean_lines)
logger.info("Summarization completed successfully (output length: %d)", len(summary))
return summary

View File

@@ -5,13 +5,13 @@ Utiliza reportlab para la generación de PDFs con soporte UTF-8.
"""
import logging
from pathlib import Path
from typing import Union
from typing import Optional, Union
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
logger = logging.getLogger(__name__)
@@ -64,6 +64,92 @@ class PDFGenerator:
.replace("\n", "<br/>")
)
def _parse_latex_table(self, lines: list[str], start_idx: int) -> tuple[Optional[Table], int]:
"""
Parsea una tabla LaTeX y la convierte a reportlab Table.
Returns:
(Table, end_index) - La tabla y el índice donde termina
"""
# Buscar begin/end tabular
table_lines = []
i = start_idx
in_table = False
while i < len(lines):
line = lines[i].strip()
if "\\begin{tabular}" in line or "begin{tabular}" in line:
in_table = True
# Extraer especificaciones de columnas
col_spec = "l"
if "{" in line:
col_spec = line.split("{")[1].split("}")[0] if "}" in line else "l"
table_lines.append({"type": "spec", "data": col_spec})
i += 1
continue
if "\\end{tabular}" in line or "end{tabular}" in line:
in_table = False
break
if in_table:
# Procesar línea de tabla
# Reemplazar & por separador y eliminar \\
row_data = line.replace("&", "|").replace("\\", "").replace("\\\\", "")
# Limpiar formato LaTeX básico
row_data = row_data.replace("hline", "").replace("\\hline", "")
cells = [c.strip() for c in row_data.split("|") if c.strip()]
if cells:
table_lines.append({"type": "row", "data": cells})
i += 1
if not table_lines:
return None, start_idx
# Convertir a Table de reportlab
data = []
col_widths = None
for tl in table_lines:
if tl["type"] == "row":
# Limpiar celdas de LaTeX
row = []
for cell in tl["data"]:
cell = cell.strip()
# Eliminar comandos LaTeX restantes
cell = cell.replace("\\textbf{", "").replace("}", "")
cell = cell.replace("\\textit{", "")
cell = cell.replace("\\emph{", "")
cell = cell.strip()
row.append(cell)
if row:
data.append(row)
if not data:
return None, start_idx
# Crear tabla
try:
num_cols = len(data[0]) if data else 1
table = Table(data)
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-0, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
return table, i
except Exception as e:
logger.warning(f"Error parsing LaTeX table: {e}")
return None, start_idx
def _parse_markdown_basic(self, markdown: str) -> list[Paragraph]:
"""
Convierte markdown básico a una lista de Paragraphs de reportlab.
@@ -101,6 +187,14 @@ class PDFGenerator:
# Línea horizontal
elif line == "---" or line == "***":
elements.append(Spacer(1, 0.2 * cm))
# Tabla LaTeX
elif "begin{tabular}" in line or "begin{tabular" in line:
latex_table, end_idx = self._parse_latex_table(lines, idx)
if latex_table:
elements.append(Spacer(1, 0.3 * cm))
elements.append(latex_table)
elements.append(Spacer(1, 0.3 * cm))
idx = end_idx - 1 # Saltar las líneas de la tabla
# Lista con guiones
elif line.startswith("- ") or line.startswith("* "):
text = self._escape_xml(line[2:])

View File

@@ -200,6 +200,26 @@ class RemoteFolderWatcher:
remote_path = f"{self.remote_path}/{filename}"
local_path = self.local_path / filename
# Verificar si ya existe el archivo localmente
if local_path.exists():
# Verificar si ya fue procesado (existe transcripción)
stem = local_path.stem
transcriptions_dir = self.local_path.parent / "transcriptions"
processed = False
if transcriptions_dir.exists():
for f in transcriptions_dir.iterdir():
if f.suffix == ".txt" and stem in f.stem:
processed = True
break
if processed:
self.logger.info(f"Skipping already processed file: {filename}")
return
else:
# Existe pero no procesado, re-descargar
self.logger.info(f"Re-downloading incomplete file: {filename}")
self.logger.info(f"Downloading: {remote_path}")
if self.webdav.download_file(remote_path, local_path):