diff --git a/core/process_manager.py b/core/process_manager.py
index 51773c8..b245f73 100644
--- a/core/process_manager.py
+++ b/core/process_manager.py
@@ -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",
diff --git a/main.py b/main.py
index 2c9945a..a0321a1 100644
--- a/main.py
+++ b/main.py
@@ -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")
diff --git a/services/ai_summary_service.py b/services/ai_summary_service.py
index 10bab40..2a70ed3 100644
--- a/services/ai_summary_service.py
+++ b/services/ai_summary_service.py
@@ -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("") or line.strip().endswith(""):
+ 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
diff --git a/services/pdf_generator.py b/services/pdf_generator.py
index aeb01a0..18bea6b 100644
--- a/services/pdf_generator.py
+++ b/services/pdf_generator.py
@@ -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", "
")
)
+ 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:])
diff --git a/watchers/folder_watcher.py b/watchers/folder_watcher.py
index f0ac646..eec143b 100644
--- a/watchers/folder_watcher.py
+++ b/watchers/folder_watcher.py
@@ -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):