diff --git a/.gitignore b/.gitignore index 24b1d2c..9d7fa29 100755 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,17 @@ old/ imperio/ check_models.py compare_configs.py + +# LaTeX auxiliary files +*.aux +*.toc +*.out +*.synctex.gz +*.fls +*.fdb_latexmk + +# Generated PDFs (keep source .tex files) +*.pdf + +# macOS specific +mac/ diff --git a/config/settings.py b/config/settings.py index c38fb8a..db236f2 100644 --- a/config/settings.py +++ b/config/settings.py @@ -138,6 +138,13 @@ class Settings: OMP_NUM_THREADS: int = int(os.getenv("OMP_NUM_THREADS", "4")) MKL_NUM_THREADS: int = int(os.getenv("MKL_NUM_THREADS", "4")) + # Parallel Processing Configuration + MAX_PARALLEL_UPLOADS: int = int(os.getenv("MAX_PARALLEL_UPLOADS", "4")) + MAX_PARALLEL_AI_REQUESTS: int = int(os.getenv("MAX_PARALLEL_AI_REQUESTS", "3")) + MAX_PARALLEL_PROCESSING: int = int(os.getenv("MAX_PARALLEL_PROCESSING", "2")) + PARALLEL_AI_STRATEGY: str = os.getenv("PARALLEL_AI_STRATEGY", "race") # race, consensus, majority + BACKGROUND_NOTION_UPLOADS: bool = os.getenv("BACKGROUND_NOTION_UPLOADS", "true").lower() == "true" + # ======================================================================== # PROPERTIES WITH VALIDATION # ======================================================================== diff --git a/document/generators.py b/document/generators.py index 5a44a41..591a001 100644 --- a/document/generators.py +++ b/document/generators.py @@ -1,352 +1,669 @@ """ -Document generation utilities +Document generation utilities - LaTeX Academic Summary System + +This module generates comprehensive academic summaries in LaTeX format +following the specifications in latex/resumen.md (the SINGLE SOURCE OF TRUTH). + +Parallel Processing: Uses multiple agents for accelerated summary generation: +- AI Provider Racing: Multiple AI providers generate in parallel +- Parallel Format Conversion: PDF + DOCX generated simultaneously +- Background Notion Uploads: Non-blocking uploads to Notion """ import logging import subprocess +import shutil import re +import threading from pathlib import Path -from typing import Dict, Any, List, Tuple +from typing import Dict, Any, Optional, Tuple, Callable +from concurrent.futures import ThreadPoolExecutor, as_completed + from core import FileProcessingError from config import settings from services.ai import ai_provider_factory +from services.ai.prompt_manager import prompt_manager + + +def _sanitize_latex(latex_code: str) -> str: + """ + Pre-process LaTeX code to fix common errors before compilation. + + This function applies automated fixes for known issues that AI models + frequently generate, reducing the need for fix_latex() iterations. + + Currently handles: + - TikZ nodes with line breaks (\\\\) missing align=center + - Unbalanced environments (best effort) + """ + if not latex_code: + return latex_code + + result = latex_code + + # Fix TikZ nodes with \\\\ but missing align=center + # Pattern: \node[...] (name) {Text\\More}; + # This is a common AI error - TikZ requires align=center for \\\\ in nodes + + # We need to find \node commands and add align=center if they have \\\\ in content + # but don't already have align= in their options + + def fix_tikz_node(match): + """Fix a single TikZ node by adding align=center if needed""" + full_match = match.group(0) + options = match.group(1) # Content inside [...] + rest = match.group(2) # Everything after options + + # Check if this node has \\\\ in its content (text between { }) + # and doesn't already have align= + if "\\\\" in rest and "align=" not in options: + # Add align=center to the options + if options.strip(): + new_options = options.rstrip() + ", align=center" + else: + new_options = "align=center" + return f"\\node[{new_options}]{rest}" + + return full_match + + # Match \node[options] followed by rest of the line + # Capture options and the rest separately + tikz_node_pattern = r"\\node\[([^\]]*)\]([^;]*;)" + result = re.sub(tikz_node_pattern, fix_tikz_node, result) + + return result class DocumentGenerator: - """Generate documents from processed text""" + """ + Generates academic summary documents in LaTeX format. - def __init__(self): + The system follows these principles: + 1. latex/resumen.md is the SINGLE SOURCE OF TRUTH for prompt structure + 2. Generates full LaTeX documents (not Markdown) + 3. Compiles to PDF using pdflatex + 4. Supports iterative fixing with AI if compilation fails + 5. Supports progress notifications via callback + """ + + def __init__(self, notification_callback: Optional[Callable[[str], None]] = None): + """ + Initialize DocumentGenerator. + + Args: + notification_callback: Optional callback function for progress notifications + Takes a single string argument (message to send) + """ self.logger = logging.getLogger(__name__) self.ai_provider = ai_provider_factory.get_best_provider() + self.notification_callback = notification_callback + self.use_parallel = ai_provider_factory.use_parallel() + self.executor = ThreadPoolExecutor(max_workers=4) + + # Ensure output directories exist + settings.LOCAL_DOWNLOADS_PATH.mkdir(parents=True, exist_ok=True) + settings.LOCAL_DOCX.mkdir(parents=True, exist_ok=True) + + if self.use_parallel: + self.logger.info( + "🚀 Parallel processing enabled: Multiple AI providers available" + ) + + def _notify(self, message: str) -> None: + """Send notification if callback is configured""" + if self.notification_callback: + try: + self.notification_callback(message) + except Exception as e: + self.logger.warning(f"Failed to send notification: {e}") + + def _generate_with_parallel_provider(self, prompt: str, **kwargs) -> str: + """ + Generate content using multiple AI providers in parallel. + + Races multiple providers and returns the first successful response, + or the best quality response if using consensus strategy. + """ + try: + parallel_provider = ai_provider_factory.get_parallel_provider(max_workers=4) + self.logger.info("🚀 Using parallel AI provider (race mode)") + + result = parallel_provider.generate_parallel( + prompt=prompt, + strategy="race", # Use first successful response + timeout_ms=300000, # 5 minutes + **kwargs, + ) + + self.logger.info( + f"✅ Parallel generation complete: {result.selected_provider} selected, " + f"{result.total_duration_ms}ms" + ) + + return result.content + + except Exception as e: + self.logger.warning( + f"⚠️ Parallel generation failed: {e}, falling back to single provider" + ) + return self.ai_provider.generate_text(prompt, **kwargs) + + def _convert_formats_parallel( + self, tex_path: Path, pdf_path: Optional[Path], base_name: str + ) -> Optional[Path]: + """ + Convert to multiple formats in parallel (DOCX, optionally PDF). + + If PDF is already compiled, only DOCX is generated. + Otherwise, both PDF and DOCX are generated in parallel. + """ + futures = {} + + # Generate DOCX + if shutil.which("pandoc"): + futures["docx"] = self.executor.submit( + self._convert_tex_to_docx, tex_path, base_name + ) + + # Wait for DOCX completion + docx_path = None + if "docx" in futures: + try: + docx_path = futures["docx"].result(timeout=60) + if docx_path: + self.logger.info(f"✅ Parallel DOCX generated: {docx_path}") + except Exception as e: + self.logger.warning(f"⚠️ DOCX generation failed: {e}") + + return docx_path + + def _upload_to_notion_background( + self, + base_name: str, + summary: str, + pdf_path: Optional[Path], + metadata: Dict[str, Any], + ): + """Upload to Notion in background thread (non-blocking).""" + + def upload_worker(): + try: + from services.notion_service import notion_service + + title = base_name.replace("_", " ").title() + notion_metadata = { + "file_type": "Audio", + "pdf_path": pdf_path or Path(""), + "add_status": False, + "use_as_page": False, + } + + page_id = notion_service.create_page_with_summary( + title=title, summary=summary, metadata=notion_metadata + ) + + if page_id: + metadata["notion_uploaded"] = True + metadata["notion_page_id"] = page_id + self.logger.info( + f"✅ Background upload to Notion complete: {title}" + ) + else: + self.logger.warning(f"⚠️ Background Notion upload failed: {title}") + + except Exception as e: + self.logger.warning(f"❌ Background Notion upload error: {e}") + + # Start background thread + thread = threading.Thread(target=upload_worker, daemon=True) + thread.start() + self.logger.info("🔄 Notion upload started in background") def generate_summary( - self, text: str, base_name: str + self, + text: str, + base_name: str, + materia: str = "Economía", + bibliographic_text: Optional[str] = None, + class_number: Optional[int] = None, ) -> Tuple[bool, str, Dict[str, Any]]: - """Generate unified summary""" - self.logger.info(f"Generating summary for {base_name}") + """ + Generate comprehensive academic summary in LaTeX format. + + Args: + text: The class transcription text + base_name: Base filename for output files + materia: Subject name (default: "Economía") + bibliographic_text: Optional supporting material from books/notes + class_number: Optional class number for header + + Returns: + Tuple of (success, summary_text, metadata) + """ + self.logger.info( + f"🚀 Starting LaTeX academic summary generation for: {base_name}" + ) + + metadata = { + "filename": base_name, + "tex_path": "", + "pdf_path": "", + "markdown_path": "", + "docx_path": "", + "summary_snippet": "", + "notion_uploaded": False, + "notion_page_id": None, + "materia": materia, + } try: - # Step 1: Generate Bullet Points (Chunking handled by provider or single prompt for now) - # Note: We use the main provider (Claude/Zai) for content generation - self.logger.info("Generating bullet points...") - bullet_prompt = f"""Analiza el siguiente texto y extrae entre 5 y 8 bullet points clave en español. + # === STEP 1: Generate LaTeX content using AI === + self.logger.info( + "🧠 Sending request to AI Provider for LaTeX generation..." + ) + self._notify("📝 Preparando prompt de resumen académico...") -REGLAS ESTRICTAS: -1. Devuelve ÚNICAMENTE bullet points, cada línea iniciando con "- " -2. Cada bullet debe ser conciso (12-20 palabras) y resaltar datos, fechas, conceptos o conclusiones importantes -3. NO agregues introducciones, conclusiones ni texto explicativo -4. Concéntrate en los puntos más importantes del texto -5. Incluye fechas, datos específicos y nombres relevantes si los hay + prompt = prompt_manager.get_latex_summary_prompt( + transcription=text, + materia=materia, + bibliographic_text=bibliographic_text, + class_number=class_number, + ) -Texto: -{text[:15000]}""" # Truncate to avoid context limits if necessary, though providers handle it differently + self._notify( + "🧠 Enviando solicitud a la IA (esto puede tardar unos minutos)..." + ) - try: - bullet_points = self.ai_provider.generate_text(bullet_prompt) - self.logger.info(f"Bullet points generated: {len(bullet_points)}") - except Exception as e: - self.logger.warning(f"Bullet point generation failed: {e}") - bullet_points = "- Puntos clave no disponibles por error en IA" + # Use parallel provider if multiple AI providers are available + if self.use_parallel: + raw_response = self._generate_with_parallel_provider(prompt) + else: + raw_response = self.ai_provider.generate_text(prompt) - # Step 2: Generate Unified Summary - self.logger.info("Generating unified summary...") - summary_prompt = f"""Eres un profesor universitario experto en historia y economía. Redacta un resumen académico integrado en español usando el texto y los bullet points extraídos. + if not raw_response: + raise FileProcessingError("AI returned empty response") -REQUISITOS ESTRICTOS DE CONTENIDO: -- Extensión entre 500-700 palabras -- Usa encabezados Markdown con jerarquía clara (##, ###) -- Desarrolla los puntos clave con profundidad y contexto histórico/económico -- Mantén un tono académico y analítico -- Incluye conclusiones significativas -- NO agregues texto fuera del resumen -- Devuelve únicamente el resumen en formato Markdown + self.logger.info(f"📝 AI response received: {len(raw_response)} characters") + self._notify(f"✅ Respuesta recibida ({len(raw_response)} caracteres)") -REQUISITOS ESTRICTOS DE FORMATO MATEMÁTICO (LaTeX): -- Si el texto incluye fórmulas matemáticas o económicas, DEBES usar formato LaTeX. -- Usa bloques $$ ... $$ para ecuaciones centradas importantes. -- Usa $ ... $ para ecuaciones en línea. -- Ejemplo: La fórmula del interés compuesto es $A = P(1 + r/n)^{{nt}}$. -- NO uses bloques de código (```latex) para las fórmulas, úsalas directamente en el texto para que Pandoc las renderice. + # === STEP 2: Extract clean LaTeX from AI response === + self._notify("🔍 Extrayendo código LaTeX...") -Contenido a resumir: -{text[:20000]} + latex_content = prompt_manager.extract_latex_from_response(raw_response) -Puntos clave a incluir obligatoriamente: -{bullet_points}""" + if not latex_content: + self.logger.warning( + "⚠️ No valid LaTeX found in response, treating as Markdown" + ) + self._notify("⚠️ No se detectó LaTeX válido, usando modo compatible...") + # Fallback to Markdown processing + return self._fallback_to_markdown(raw_response, base_name, metadata) - try: - raw_summary = self.ai_provider.generate_text(summary_prompt) - except Exception as e: - self.logger.error(f"Raw summary generation failed: {e}") - raise e + self.logger.info("✨ Valid LaTeX content detected") + self._notify(f"✨ LaTeX detectado: {len(latex_content)} caracteres") - # Step 3: Format with IA (using main provider instead of Gemini) - self.logger.info("Formatting summary with IA...") - format_prompt = f"""Revisa y mejora el siguiente resumen en Markdown para que sea perfectamente legible y compatible con Pandoc: + # === STEP 3: Compilation Loop with Self-Correction === + max_retries = 3 + current_latex = latex_content -{raw_summary} + for attempt in range(max_retries + 1): + # Sanitize LaTeX before saving (fix common AI errors like TikZ nodes) + current_latex = _sanitize_latex(current_latex) -Instrucciones: -- Corrige cualquier error de formato Markdown -- Asegúrate de que los encabezados estén bien espaciados -- Verifica que las viñetas usen "- " correctamente -- Mantén exactamente el contenido existente -- EVITA el uso excesivo de negritas (asteriscos), úsalas solo para conceptos clave -- VERIFICA que todas las fórmulas matemáticas estén correctamente encerradas en $...$ (inline) o $$...$$ (display) -- NO alteres la sintaxis LaTeX dentro de los delimitadores $...$ o $$...$$ -- Devuelve únicamente el resumen formateado sin texto adicional""" + # Save current .tex file + self._notify( + f"📄 Guardando archivo .tex (intento {attempt + 1}/{max_retries + 1})..." + ) - try: - # Use the main provider (Claude/GLM) for formatting too - if self.ai_provider.is_available(): - summary = self.ai_provider.generate_text(format_prompt) - else: - self.logger.warning( - "AI provider not available for formatting, using raw summary" + tex_path = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.tex" + tex_path.write_text(current_latex, encoding="utf-8") + metadata["tex_path"] = str(tex_path) + + # Try to compile + self._notify("⚙️ Primera pasada de compilación LaTeX...") + + pdf_path = self._compile_latex( + tex_path, output_dir=settings.LOCAL_DOWNLOADS_PATH + ) + + if pdf_path: + self.logger.info( + f"✅ Compilation success on attempt {attempt + 1}!" ) - summary = raw_summary - except Exception as e: - self.logger.warning(f"Formatting failed ({e}), using raw summary") - summary = raw_summary + self._notify("✅ PDF generado exitosamente!") + metadata["pdf_path"] = str(pdf_path) - # Generate filename - filename = self._generate_filename(text, summary) + # Generate DOCX in parallel + self._notify("📄 Generando archivo DOCX en paralelo...") + docx_path = self._convert_formats_parallel( + tex_path, pdf_path, base_name + ) + if docx_path: + self._notify("✅ DOCX generado exitosamente!") + metadata["docx_path"] = str(docx_path) - # Create document - markdown_path = self._create_markdown(summary, base_name) + # Create a text summary for Notion/preview + text_summary = self._create_text_summary(current_latex) + metadata["summary_snippet"] = text_summary[:500] + "..." - docx_path = None - try: - docx_path = self._create_docx(markdown_path, base_name) - except Exception as e: - self.logger.error(f"Failed to create DOCX (non-critical): {e}") + # Upload to Notion in background if configured + if settings.has_notion_config: + self._notify("📤 Iniciando carga a Notion en segundo plano...") + self._upload_to_notion_background( + base_name=base_name, + summary=text_summary, + pdf_path=pdf_path, + metadata=metadata, + ) - pdf_path = None - try: - # Sanitize LaTeX before PDF generation - self._sanitize_latex(markdown_path) - pdf_path = self._create_pdf(markdown_path, base_name) - except Exception as e: - self.logger.error(f"Failed to create PDF (non-critical): {e}") + self._notify("🎉 ¡Resumen completado con éxito!") + return True, text_summary, metadata - # Upload to Notion if configured + # Compilation failed - ask AI to fix + if attempt < max_retries: + self.logger.warning( + f"⚠️ Compilation failed (Attempt {attempt + 1}/{max_retries + 1}). " + f"Requesting AI fix..." + ) + self._notify( + f"⚠️ Error de compilación ({attempt + 1}/{max_retries + 1}), solicitando corrección a IA..." + ) + + # Get error log + log_file = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.log" + error_log = "Log file not found" + if log_file.exists(): + error_log = log_file.read_text( + encoding="utf-8", errors="ignore" + )[-2000:] + + # Ask AI to fix + try: + self._notify("🔧 La IA está corrigiendo el código LaTeX...") + if hasattr(self.ai_provider, "fix_latex"): + fixed_latex = self.ai_provider.fix_latex( + current_latex, error_log + ) + cleaned = prompt_manager.extract_latex_from_response( + fixed_latex + ) + if cleaned: + current_latex = cleaned + else: + current_latex = fixed_latex + self._notify( + "✅ Código LaTeX corregido, reintentando compilación..." + ) + else: + self.logger.error( + "❌ AI provider doesn't support fix_latex()" + ) + break + except Exception as e: + self.logger.error(f"❌ AI fix request failed: {e}") + break + else: + self.logger.error( + "❌ Max retries reached. LaTeX compilation failed." + ) + self._notify( + "❌ No se pudo compilar el LaTeX después de varios intentos" + ) + + # If we get here, all compilation attempts failed + self._notify("⚠️ Usando modo de compatibilidad Markdown...") + return self._fallback_to_markdown( + current_latex or raw_response, base_name, metadata + ) + + except Exception as e: + self.logger.error( + f"❌ Critical error in document generation: {e}", exc_info=True + ) + self._notify(f"❌ Error en la generación: {str(e)[:100]}") + return False, "", metadata + + def _compile_latex(self, tex_path: Path, output_dir: Path) -> Optional[Path]: + """ + Compile LaTeX to PDF using pdflatex. Runs twice for TOC. + + Args: + tex_path: Path to .tex file + output_dir: Directory for output files + + Returns: + Path to generated PDF or None if failed + """ + base_name = tex_path.stem + expected_pdf = output_dir / f"{base_name}.pdf" + + # Check if pdflatex is available + if not shutil.which("pdflatex"): + self.logger.error("🚫 pdflatex not found in system PATH") + return None + + cmd = [ + "pdflatex", + "-interaction=nonstopmode", + "-halt-on-error", + f"-output-directory={output_dir}", + str(tex_path), + ] + + try: + # Pass 1 + self.logger.info("⚙️ Compiling LaTeX (Pass 1/2)...") + subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + timeout=120, + ) + + # Pass 2 (for TOC resolution) + self.logger.info("⚙️ Compiling LaTeX (Pass 2/2)...") + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + timeout=120, + ) + + if result.returncode == 0 and expected_pdf.exists(): + self.logger.info(f"✅ PDF generated: {expected_pdf}") + self._cleanup_latex_aux(output_dir, base_name) + return expected_pdf + else: + # Read log file for error info + log_file = output_dir / f"{base_name}.log" + error_snippet = "Unknown error" + if log_file.exists(): + try: + log_content = log_file.read_text( + encoding="utf-8", errors="ignore" + ) + errors = [ + line + for line in log_content.splitlines() + if line.startswith("!") + ] + if errors: + error_snippet = errors[0][:200] + except: + pass + + self.logger.error(f"❌ LaTeX compilation failed: {error_snippet}") + return None + + except subprocess.TimeoutExpired: + self.logger.error("❌ LaTeX compilation timed out") + return None + except Exception as e: + self.logger.error(f"❌ Error during LaTeX execution: {e}") + return None + + def _convert_tex_to_docx(self, tex_path: Path, base_name: str) -> Optional[Path]: + """Convert .tex to .docx using Pandoc.""" + if not shutil.which("pandoc"): + self.logger.warning("⚠️ pandoc not found, skipping DOCX generation") + return None + + docx_path = settings.LOCAL_DOCX / f"{base_name}.docx" + cmd = ["pandoc", str(tex_path), "-o", str(docx_path)] + + try: + subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60) + self.logger.info(f"✅ DOCX generated: {docx_path}") + return docx_path + except Exception as e: + self.logger.warning(f"⚠️ DOCX generation failed: {e}") + return None + + def _create_text_summary(self, latex_content: str) -> str: + """Extract a plain text summary from LaTeX content for Notion/preview.""" + # Remove LaTeX commands and keep content + text = latex_content + + # Remove document class and packages + text = re.sub(r"\\documentclass\[?[^\]]*\]?\{[^\}]+\}", "", text) + text = re.sub(r"\\usepackage\{[^\}]+\}", "", text) + text = re.sub(r"\\geometry\{[^\}]+\}", "", text) + text = re.sub(r"\\pagestyle\{[^\}]+\}", "", text) + text = re.sub(r"\\fancyhf\{\}", "", text) + text = re.sub(r"\\fancyhead\[?[^\]]*\]?\{[^\}]+\}", "", text) + text = re.sub(r"\\fancyfoot\[?[^\]]*\]?\{[^\}]+\}", "", text) + + # Convert sections to markdown-style + text = re.sub(r"\\section\*?\{([^\}]+)\}", r"# \1", text) + text = re.sub(r"\\subsection\*?\{([^\}]+)\}", r"## \1", text) + text = re.sub(r"\\subsubsection\*?\{([^\}]+)\}", r"### \1", text) + + # Remove tcolorbox environments (keep content) + text = re.sub( + r"\\begin\{(definicion|importante|ejemplo)\}\[?[^\]]*\]?", + r"\n**\1:** ", + text, + ) + text = re.sub(r"\\end\{(definicion|importante|ejemplo)\}", "", text) + + # Convert itemize to bullets + text = re.sub(r"\\item\s*", "- ", text) + text = re.sub(r"\\begin\{(itemize|enumerate)\}", "", text) + text = re.sub(r"\\end\{(itemize|enumerate)\}", "", text) + + # Clean up math (basic) + text = re.sub(r"\$\$([^\$]+)\$\$", r"\n\n\1\n\n", text) + text = re.sub(r"\$([^\$]+)\$", r"\1", text) + + # Remove remaining LaTeX commands + text = re.sub(r"\\[a-zA-Z]+(\{[^\}]*\})*", "", text) + text = re.sub(r"[{}]", "", text) + + # Clean whitespace + text = re.sub(r"\n\s*\n\s*\n", "\n\n", text) + text = text.strip() + + return text + + def _fallback_to_markdown( + self, content: str, base_name: str, metadata: Dict[str, Any] + ) -> Tuple[bool, str, Dict[str, Any]]: + """Fallback when LaTeX generation fails.""" + self.logger.warning("⚠️ Falling back to Markdown processing") + + md_path = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}_resumen.md" + md_path.write_text(content, encoding="utf-8") + metadata["markdown_path"] = str(md_path) + + # Try to convert to PDF via pandoc + if shutil.which("pandoc"): + pdf_path = self._convert_md_to_pdf(md_path, base_name) + if pdf_path: + metadata["pdf_path"] = str(pdf_path) + + docx_path = self._convert_md_to_docx(md_path, base_name) + if docx_path: + metadata["docx_path"] = str(docx_path) + + metadata["summary_snippet"] = content[:500] + "..." + return True, content, metadata + + def _convert_md_to_pdf(self, md_path: Path, base_name: str) -> Optional[Path]: + """Convert Markdown to PDF using pandoc.""" + pdf_path = settings.LOCAL_DOWNLOADS_PATH / f"{base_name}.pdf" + cmd = [ + "pandoc", + str(md_path), + "-o", + str(pdf_path), + "--pdf-engine=pdflatex", + "-V", + "geometry:margin=2.5cm", + ] + + try: + subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60) + self.logger.info(f"✅ PDF from Markdown: {pdf_path}") + return pdf_path + except Exception as e: + self.logger.warning(f"⚠️ PDF from Markdown failed: {e}") + return None + + def _convert_md_to_docx(self, md_path: Path, base_name: str) -> Optional[Path]: + """Convert Markdown to DOCX using pandoc.""" + docx_path = settings.LOCAL_DOCX / f"{base_name}.docx" + cmd = ["pandoc", str(md_path), "-o", str(docx_path)] + + try: + subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=60) + self.logger.info(f"✅ DOCX from Markdown: {docx_path}") + return docx_path + except Exception as e: + self.logger.warning(f"⚠️ DOCX from Markdown failed: {e}") + return None + + def _cleanup_latex_aux(self, output_dir: Path, base_name: str): + """Clean up auxiliary LaTeX files.""" + extensions = [".aux", ".log", ".out", ".toc"] + for ext in extensions: + aux_file = output_dir / f"{base_name}{ext}" + if aux_file.exists(): + try: + aux_file.unlink() + except: + pass + + def _upload_to_notion( + self, + base_name: str, + summary: str, + pdf_path: Optional[Path], + metadata: Dict[str, Any], + ): + """Upload summary to Notion if configured.""" + try: from services.notion_service import notion_service - notion_uploaded = False - notion_page_id = None - if settings.has_notion_config: - try: - title = base_name.replace("_", " ").title() + title = base_name.replace("_", " ").title() + notion_metadata = { + "file_type": "Audio", + "pdf_path": pdf_path or Path(""), + "add_status": False, + "use_as_page": False, + } - # Crear página con el contenido completo del resumen - notion_metadata = { - "file_type": "Audio", # O 'PDF' dependiendo del origen - "pdf_path": pdf_path if pdf_path else Path(""), - "add_status": False, # No usar Status/Tipo (no existen en la DB) - "use_as_page": False, # Usar como database, no página - } + page_id = notion_service.create_page_with_summary( + title=title, summary=summary, metadata=notion_metadata + ) - notion_page_id = notion_service.create_page_with_summary( - title=title, summary=summary, metadata=notion_metadata - ) - - if notion_page_id: - notion_uploaded = True - self.logger.info( - f"✅ Resumen subido a Notion: {title} (ID: {notion_page_id})" - ) - else: - self.logger.warning(f"⚠️ No se pudo subir a Notion: {title}") - except Exception as e: - self.logger.warning(f"❌ Error al subir a Notion: {e}") - import traceback - - traceback.print_exc() + if page_id: + metadata["notion_uploaded"] = True + metadata["notion_page_id"] = page_id + self.logger.info(f"✅ Uploaded to Notion: {title}") else: - self.logger.info("Notion not configured - skipping upload") - - metadata = { - "markdown_path": str(markdown_path), - "docx_path": str(docx_path) if docx_path else "", - "pdf_path": str(pdf_path) if pdf_path else "", - "docx_name": Path(docx_path).name if docx_path else "", - "summary": summary, - "filename": filename, - "notion_uploaded": notion_uploaded, - "notion_page_id": notion_page_id, - } - - return True, summary, metadata + self.logger.warning(f"⚠️ Notion upload failed: {title}") except Exception as e: - self.logger.error(f"Document generation process failed: {e}") - return False, "", {} - - def _sanitize_latex(self, markdown_path: Path) -> None: - """Sanitize LaTeX syntax in Markdown file to prevent Pandoc errors""" - try: - content = markdown_path.read_text(encoding="utf-8") - - # 1. Unescape escaped dollar signs which are common LLM errors for math - content = content.replace(r"\$", "$") - - # 2. Fix common Cyrillic and Greek characters that sneak in via LLMs - replacements = { - "ч": "ch", - "в": "v", - "к": "k", - "м": "m", - "н": "n", - "т": "t", - "—": "-", - "–": "-", - "“": '"', - "”": '"', - "’": "'", - "Δ": "$\\Delta$", - "δ": "$\\delta$", - "Σ": "$\\Sigma$", - "σ": "$\\sigma$", - "π": "$\\pi$", - "Π": "$\\Pi$", - "α": "$\\alpha$", - "β": "$\\beta$", - "γ": "$\\gamma$", - "θ": "$\\theta$", - "λ": "$\\lambda$", - "μ": "$\\mu$", - } - - # Be careful not to double-replace already correct LaTeX - for char, repl in replacements.items(): - if char in content: - # Check if it's already inside math mode would be complex, - # but for now we assume raw unicode greek chars should become latex - content = content.replace(char, repl) - - markdown_path.write_text(content, encoding="utf-8") - self.logger.info(f"Sanitized LaTeX in {markdown_path}") - except Exception as e: - self.logger.warning(f"Failed to sanitize LaTeX: {e}") - - def _generate_filename(self, text: str, summary: str) -> str: - """Generate intelligent filename""" - try: - # Use AI to extract key topics - prompt = f"""Extract 2-3 key topics from this summary to create a filename. -Summary: {summary} - -Return only the topics separated by hyphens, max 20 chars each, in Spanish:""" - - try: - topics_text = self.ai_provider.generate_text(prompt) - except Exception: - topics_text = summary[:100] - - # Simple topic extraction - topics = re.findall(r"\b[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+\b", topics_text)[:3] - if not topics: - topics = ["documento"] - - # Limit topic length - topics = [t[: settings.MAX_FILENAME_TOPICS_LENGTH] for t in topics] - - filename = "_".join(topics)[: settings.MAX_FILENAME_LENGTH] - return filename - - except Exception as e: - self.logger.error(f"Filename generation failed: {e}") - return "documento" - - def _create_markdown(self, summary: str, base_name: str) -> Path: - """Create Markdown document""" - output_dir = settings.LOCAL_DOWNLOADS_PATH - output_dir.mkdir(parents=True, exist_ok=True) - - output_path = output_dir / f"{base_name}_unificado.md" - - content = f"""# {base_name.replace("_", " ").title()} - -## Resumen - -{summary} - ---- - -*Generado por CBCFacil* -""" - - with open(output_path, "w", encoding="utf-8") as f: - f.write(content) - - return output_path - - def _create_docx(self, markdown_path: Path, base_name: str) -> Path: - """Create DOCX document using pandoc""" - output_dir = settings.LOCAL_DOCX - output_dir.mkdir(parents=True, exist_ok=True) - - output_path = output_dir / f"{base_name}_unificado.docx" - - self.logger.info( - f"Converting Markdown to DOCX: {markdown_path} -> {output_path}" - ) - - try: - cmd = [ - "pandoc", - str(markdown_path), - "-o", - str(output_path), - "--from=markdown", - "--to=docx", - ] - - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - - self.logger.info("DOCX generated successfully with pandoc") - return output_path - - except subprocess.CalledProcessError as e: - self.logger.error(f"Pandoc DOCX conversion failed: {e.stderr}") - raise FileProcessingError(f"Failed to generate DOCX: {e.stderr}") - except Exception as e: - self.logger.error(f"Error generating DOCX: {e}") - raise FileProcessingError(f"Error generating DOCX: {e}") - - def _create_pdf(self, markdown_path: Path, base_name: str) -> Path: - """Create PDF document using pandoc and pdflatex""" - output_dir = settings.LOCAL_DOWNLOADS_PATH - output_dir.mkdir(parents=True, exist_ok=True) - - output_path = output_dir / f"{base_name}_unificado.pdf" - - self.logger.info( - f"Converting Markdown to PDF: {markdown_path} -> {output_path}" - ) - - try: - cmd = [ - "pandoc", - str(markdown_path), - "-o", - str(output_path), - "--pdf-engine=pdflatex", - "-V", - "geometry:margin=2.5cm", - "-V", - "fontsize=12pt", - "--highlight-style=tango", - ] - - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - - self.logger.info("PDF generated successfully with pandoc") - return output_path - - except subprocess.CalledProcessError as e: - self.logger.error(f"Pandoc PDF conversion failed: {e.stderr}") - raise FileProcessingError(f"Failed to generate PDF: {e.stderr}") - except Exception as e: - self.logger.error(f"Error generating PDF: {e}") - raise FileProcessingError(f"Error generating PDF: {e}") + self.logger.warning(f"❌ Notion upload error: {e}") diff --git a/latex/clase_revolucion_rusa_crisis_30.tex b/latex/clase_revolucion_rusa_crisis_30.tex new file mode 100644 index 0000000..f4ca817 --- /dev/null +++ b/latex/clase_revolucion_rusa_crisis_30.tex @@ -0,0 +1,447 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[spanish,provide=*]{babel} +\usepackage{amsmath,amssymb} +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,shapes.geometric,calc} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{fancyhdr} +\usepackage{titlesec} +\usepackage{tcolorbox} +\usepackage{array} +\usepackage{multirow} + +\geometry{margin=2.5cm} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{Economía - CBC} +\fancyhead[R]{Clase: Revolución Rusa y Crisis del 30} +\fancyfoot[C]{\thepage} + +% Cajas para destacar contenido +\newtcolorbox{definicion}[1][]{ + colback=blue!5!white, + colframe=blue!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{importante}[1][]{ + colback=red!5!white, + colframe=red!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{ejemplo}[1][]{ + colback=green!5!white, + colframe=green!50!black, + fonttitle=\bfseries, + title=#1 +} + +\title{\textbf{Revolución Rusa y Crisis del 30}\\ +\large Ciclos Económicos, Socialismo y la Gran Depresión} +\author{CBC - UBA} +\date{\today} + +\begin{document} + +\maketitle +\tableofcontents +\newpage + +\section{Introducción} + +La presente clase aborda dos de los procesos económicos más transformadores del siglo XX: la \textbf{Revolución Rusa} y la \textbf{Gran Depresión de 1929}. Ambos eventos representan puntos de inflexión en la historia económica mundial y dan lugar a nuevas formas de organización económica, así como a teorías económicas que buscan explicar y solucionar las crisis capitalistas. + +El contexto de la Revolución Rusa se sitúa en un imperio zarista en crisis, donde las ideas marxistas encuentran terreno fértil tras años de marginalización del proletariado industrial y rural. Por otro lado, la Crisis del 30 representa el colapso del modelo económico liberal y el surgimiento de nuevas teorías intervencionistas. + +\section{Contexto Histórico: El Siglo XIX y las Ideas Revolucionarias} + +\subsection{El surgimiento del proletariado y las ideas marxistas} + +Durante la década de 1830-1840, se observó un fenómeno social particular: la \textbf{invención del proletariado} por parte de la burguesía media. Este término se refiere a la clase trabajadora industrial que surgió con la Revolución Industrial. + +\begin{definicion}[Proletariado] +El proletariado es la clase social que carece de medios de producción y debe vender su fuerza de trabajo para subsistir. Surge con la industrialización y la concentración de la propiedad en manos de la burguesía. +\end{definicion} + +Según Kovalevsky, \textit{"el comunismo se asoma sobre Europa"} en este período. Los trabajadores comenzaron a enarbolar la \textbf{bandera roja} en lugar de las banderas nacionales francesas, simbolizando la identificación con la ideología marxista. + +\subsection{El crecimiento económico y el paréntesis revolucionario} + +El crecimiento económico impulsado por la \textbf{Revolución de los Transportes} (ferrocarriles, barcos a vapor) puso temporalmente en reposo las ideas revolucionarias. La expansión económica generó empleos y mejoró las condiciones de vida, postergando las tensiones sociales. + +\begin{importante}[Idea clave] +El crecimiento económico funciona como un \textit{paréntesis} para las ideas revolucionarias. Es necesario esperar la Gran Depresión para que estas ideas resurjan con fuerza. +\end{importante} + +\subsection{La disyuntiva del marxismo: socialdemocracia vs. revolución} + +Con el desarrollo de las ideas marxistas, surgió una disyuntiva fundamental: + +\begin{itemize} +\item \textbf{Socialdemocracia}: Acceder al poder político a través del sistema democrático y utilizar el Estado para generar condiciones de igualdad y redistribución. +\item \textbf{Revolución proletaria}: Toma del poder por la fuerza de la clase trabajadora para establecer la dictadura del proletariado. +\end{itemize} + +\section{Estados de Bienestar vs. Estados Intervencionistas} + +Es fundamental distinguir entre dos tipos de intervención estatal que surgieron en este período: + +\begin{table}[h] +\centering +\begin{tabular}{@{}p{0.45\textwidth}@{}p{0.45\textwidth}@{}} +\toprule +\textbf{Estado Intervencionista} & \textbf{Estado de Bienestar} \\ +\midrule +Protege el producto nacional ante la competencia extranjera & Protege a los trabajadores y busca el bienestar de la población \\ +Implementa aranceles y barreras comerciales & Garantiza derechos laborales y seguridad social \\ +Fomenta la industria nacional & Provee servicios de salud, educación y vivienda \\ +\bottomrule +\end{tabular} +\caption{Comparación entre Estado Intervencionista y Estado de Bienestar} +\end{table} + +\subsection{Conquistas laborales del período} + +Las luchas obreras de este período lograron conquistas fundamentales: + +\begin{itemize} +\item \textbf{Jornada de 8 horas}: 8 horas de trabajo, 8 horas de descanso, 8 horas de libre disposición +\item \textbf{Protección infantil}: Limitación del trabajo infantil +\item \textbf{Protección materna}: Derechos para mujeres embarazadas +\item \textbf{Condiciones de trabajo}: Mejoras en seguridad y salubridad +\end{itemize} + +\section{La Revolución Rusa} + +\subsection{Antecedentes: la Rusia zarista en crisis} + +La Rusia zarista presentaba características particulares que la diferenciaban de otras potencias europeas: + +\begin{itemize} +\item \textbf{Economía predominantemente agraria}: 80\% de la economía era agrícola a fines del siglo XIX +\item \textbf{Industrialización tardía e incompleta}: A diferencia de Estados Unidos y Alemania, Rusia no se había industrializado significativamente +\item \textbf{Crisis agraria}: Durante la Gran Depresión, hubo hambrunas generalizadas +\item \textbf{Participación en la Primera Guerra Mundial}: Rusia no tuvo un buen desempeño bélico +\end{itemize} + +\begin{importante}[Contradicción fundamental] +La teoría marxista preveía una revolución proletaria industrial, pero Rusia era un país 80\% agrícola. Esta contradicción es central para entender el desarrollo posterior de la revolución. +\end{importante} + +\subsection{Las leyes de Marx y la situación rusa} + +Las tres leyes de Marx parecían cumplirse en Rusia: + +\begin{enumerate} +\item \textbf{Caída de precios} $\rightarrow$ \textbf{Caída de la tasa de beneficio} +\item \textbf{Caída de la tasa de beneficio} $\rightarrow$ \textbf{Caída de la producción} +\item \textbf{Caída de la producción} $\rightarrow$ \textbf{Aumento del desempleo} +\item \textbf{Aumento del desempleo} $\rightarrow$ \textbf{Descontento social} $\rightarrow$ \textbf{Revolución} +\end{enumerate} + +\subsection{La válvula de escape: la migración} + +En Europa, la migración masiva a América funcionó como una válvula de escape que redujo las tensiones sociales. Millones de personas cruzaron el océano para trabajar, evitando que las tensiones políticas alcanzaran niveles críticos. + +\begin{ejemplo}[Migración italiana] +El campesino italiano viajaba 15.000 kilómetros para cosechar meta en América y volver a Italia con ahorros. Esta migración evitó concentraciones demográficas que hubieran alimentado el conflicto social. +\end{ejemplo} + +\subsection{El proceso revolucionario} + +\subsubsection{Caída del Zar Nicolás II} + +El zar Nicolás II perdió legitimidad ante la sociedad. La Primera Guerra Mundial exacerbó los problemas económicos y generó hambruna tanto en las ciudades como en el frente de batalla. + +\subsubsection{Revolución de febrero de 1917} + +Liderada por \textbf{Vladimir Lenin}, esta revolución inicial derrocó al zar. Sin embargo, existe una contradicción fundamental: la revolución fue apoyada principalmente por el campo, no por los trabajadores industriales. + +\begin{definicion}[Válvula de escape histórica] +La migración masiva a América funcionó como una válvula de escape que redujo las tensiones sociales en Europa. Al permitir que millones de personas encontraran trabajo en el Nuevo Mundo, se evitó que las tensiones políticas alcanzaran niveles críticos. +\end{definicion} + +\section{Tres Etapas de la Revolución Rusa} + +La Revolución Rusa se desarrolló en tres etapas bien diferenciadas, cada una con políticas económicas específicas. + +\subsection{Primera Etapa: Comunismo de Guerra (1918-1921)} + +El Comunismo de Guerra implementó medidas radicales de transformación económica: + +\begin{enumerate} +\item \textbf{Expropiación de tierras}: Las tierras de la nobleza y grandes terratenientes fueron expropiadas y entregadas \textit{en propiedad privada} a los campesinos que apoyaron la revolución. + +\begin{importante}[Primera contradicción] +La revolución socialista creó propietarios privados en el campo. Esto contradice el principio marxista de abolición de la propiedad privada. +\end{importante} + +\item \textbf{Control de las empresas}: Los trabajadores tomaron el control de las empresas, expropiando a los propietarios anteriores. + +\item \textbf{Creación de los Soviets}: Consejos de delegados de las diferentes fábricas que decidían sobre producción, destino y productividad. + +\item \textbf{Consejo económico supremo}: Organismo central para dirigir el comercio interno, externo y las relaciones comerciales. + +\item \textbf{Nacionalización de la banca}: Los bancos más importantes fueron nacionalizados. + +\item \textbf{Desconocimiento de la deuda externa}: La Unión Soviética desconoció la deuda externa contraída por el zar. +\end{enumerate} + +\subsubsection{Resultados del Comunismo de Guerra} + +\begin{itemize} +\item \textbf{Caída de la productividad agrícola}: Al dividir las grandes haciendas en parcelas pequeñas, la producción disminuyó +\item \textbf{Hambruna persistente}: Los problemas para alimentar a las ciudades continuaron +\item \textbf{Represión estatal}: El Estado comenzó a sancionar, reprimir y requisar producción +\item \textbf{Estancamiento industrial}: Sin capital para invertir en maquinaria agrícola, la industria pesada no se desarrolló +\end{itemize} + +\subsection{Segunda Etapa: Nueva Política Económica - NEP (1921-1928)} + +Ante el fracaso del Comunismo de Guerra, se implementó la NEP bajo el liderazgo de Lenin: + +\begin{definicion}[NEP - Nueva Política Económica] +La NEP consistió en otorgar libertad de mercado al sector agrícola dentro de una revolución marxista. Los campesinos podían vender su producción en el mercado libre, determinando precios y destinos. +\end{definicion} + +\textbf{Objetivo de la NEP:} +\begin{itemize} +\item Reactivar la producción agrícola +\item Generar excedentes que volcar a la industrialización +\item Crear un círculo virtuoso: más agricultura $\rightarrow$ más demanda de maquinaria $\rightarrow$ más industria $\rightarrow$ más empleo $\rightarrow$ más consumo +\end{itemize} + +\subsubsection{La contradicción de la NEP} + +\begin{importante}[Renuncia a la industrialización acelerada] +La NEP implicaba renunciar a la industrialización acelerada porque requería esperar el \textit{"derrame"} desde el campo. La transición del feudalismo al capitalismo había demorado dos siglos; la URSS no podía esperar tanto. +\end{importante} + +\subsubsection{La grieta en el partido bolchevique} + +Se abrió una división interna: + +\begin{table}[h] +\centering +\begin{tabular}{@{}p{0.45\textwidth}@{}p{0.45\textwidth}@{}} +\toprule +\textbf{Defensores de la NEP} & \textbf{Partidarios de industrialización acelerada} \\ +\midrule +Esperar el desarrollo del campo & Industrialización forzada desde el Estado \\ +Derrame espontáneo del capital & Planificación centralizada \\ +Stalin (luego de cambiar de posición) & Trotsky \\ +\bottomrule +\end{tabular} +\caption{Divisiones en el partido bolchevique} +\end{table} + +\subsection{Tercera Etapa: Colectivización e Industrialización Forzada (1928-1941)} + +Bajo el liderazgo de \textbf{Iósif Stalin}, se implementó la colectivización forzosa: + +\begin{itemize} +\item \textbf{Colectivización de la tierra}: Las parcelas privadas fueron reunidas en granjas estatales (\textit{koljoses} y \textit{sovjoses}) +\item \textbf{Eliminación de la propiedad privada}: El campesino dueño de una parcela la perdió en favor del Estado +\item \textbf{Industrialización pesada}: Todos los recursos se volcaron a la industria pesada +\end{itemize} + +\begin{importante}[Costo humano] +La colectivización causó millones de muertes. Sumado a las víctimas de la Primera Guerra Mundial, la Guerra Civil y la Segunda Guerra Mundial, el costo humano de la transformación soviética fue enorme. +\end{importante} + +\subsubsection{Resultados de la colectivización} + +\begin{itemize} +\item 75\% del comercio interno en manos del Estado +\item Banca 100\% estatal +\item Solo 3\% del sector agrícola permaneció en manos privadas +\item Mientras Occidente se hundía en la Gran Depresión, la URSS entraba en la senda de la industrialización pesada +\end{itemize} + +\section{La Gran Depresión de 1929} + +\subsection{Contexto: los gloriosos años 20 en Estados Unidos} + +Estados Unidos emergió de la Primera Guerra Mundial como: +\begin{itemize} +\item Uno de los mayores beneficiados del conflicto +\item Mayor exportador de manufacturas +\item Nueva potencia financiera (desplazó a Inglaterra) +\end{itemize} + +La década de 1920 se caracterizó por: +\begin{itemize} +\item Crecimiento económico aparentemente infinito +\item Aumento del salario real del trabajador promedio +\item Expansión del consumo a crédito +\item Especulación bursátil +\end{itemize} + +\subsection{Causales de la crisis (no solo el crack)} + +Es fundamental entender que el \textbf{Jueves Negro} (caída de la bolsa) no fue la causa, sino el detonante de una serie de procesos: + +\subsubsection{Políticas que inflaron la burbuja} + +\begin{enumerate} +\item \textbf{Vuelta al proteccionismo}: Estados Unidos volvió a su política tradicional de proteger su mercado interno con altos aranceles, cerrando mercados a las exportaciones europeas. + +\item \textbf{Aumento de tasas de interés}: La Reserva Federal aumentó las tasas, atrayendo capitales de todo el mundo hacia Estados Unidos. + +\begin{ejemplo}[Fuga de capitales] +Los capitales que estaban invertidos en Alemania y Latinoamérica se retiraron masivamente para buscar mayores retornos en Estados Unidos. Esto provocó crisis económicas en la periferia. +\end{ejemplo} + +\item \textbf{Especulación bursátil}: Los capitales no se volcaron a la producción real (fábricas, empleo), sino a la especulación con acciones. + +\item \textbf{Economía frágil en el crédito}: Tanto la economía norteamericana como la mundial dependían excesivamente del crédito. +\end{enumerate} + +\subsection{El ciclo de la crisis} + +\begin{center} +\begin{tikzpicture}[ + node distance=1.5cm, + auto, + block/.style={rectangle, draw, fill=blue!10, text width=6cm, text centered, rounded corners, minimum height=1cm}, + arrow/.style={-Stealth, thick} +] +\node [block] (tasa) {Aumento de tasas de interés en EE.UU.}; +\node [block, below=of tasa] (fuga) {Fuga de capitales de periferia y Europa}; +\node [block, below=of fuga] (crisis) {Crisis económica en Europa y Latinoamérica}; +\node [block, below=of crisis] (reduccion) {Reducción de importaciones de manufacturas}; +\node [block, below=of reduccion] (sobreproduccion) {Sobreproducción en EE.UU.}; +\node [block, below=of sobreproduccion] (desempleo) {Caída de producción $\rightarrow$ Desempleo}; +\node [block, below=of desempleo, fill=red!10] (especulacion) {Especulación bursátil (capitales sin producción real)}; +\node [block, below=of especulacion, fill=red!20] (crash) {CRACK DEL JUEVES NEGRO}; + +\draw [arrow] (tasa) -- (fuga); +\draw [arrow] (fuga) -- (crisis); +\draw [arrow] (crisis) -- (reduccion); +\draw [arrow] (reduccion) -- (sobreproduccion); +\draw [arrow] (sobreproduccion) -- (desempleo); +\draw [arrow] (desempleo) -- (especulacion); +\draw [arrow] (especulacion) -- (crash); +\end{tikzpicture} +\end{center} + +\subsection{Consecuencias del crack} + +\subsubsection{Pánico bancario} + +La desconfianza se contagió del mercado bursátil a los bancos: + +\begin{enumerate} +\item Los tenedores de acciones vendieron masivamente +\item La desconfianza se extendió a los depositantes bancarios +\item Retiros masivos de ahorros (corridas bancarias) +\item Los bancos no tenían fondos para hacer frente a los retiros +\item Más de la mitad de los bancos de EE.UU. quebraron +\end{enumerate} + +\subsubsection{Efectos reales de la Gran Depresión} + +\begin{itemize} +\item \textbf{Caída de la producción}: Las empresas quebraron por falta de liquidez +\item \textbf{Desempleo masivo}: El desempleo llegó al 25\% +\item \textbf{Caída del consumo}: De la prosperidad de los años 20 a filas por un plato de comida +\item \textbf{Pérdida de ahorros}: Los trabajadores perdieron sus ahorros depositados en bancos quebrados +\end{itemize} + +\begin{importante}[Distinción crucial] +Es fundamental distinguir entre las \textbf{explicaciones} de la crisis (teorías económicas que surgieron para explicarla) y las \textbf{causales} de la crisis (los procesos concretos que la provocaron). +\end{importante} + +\section{Comparación: Revolución Rusa vs. Gran Depresión} + +\begin{table}[h] +\centering +\small +\begin{tabular}{@{}p{0.45\textwidth}@{}p{0.45\textwidth}@{}} +\toprule +\textbf{Revolución Rusa} & \textbf{Gran Depresión} \\ +\midrule +Crisis en economía agrícola atrasada & Crisis en economía industrial avanzada \\ +Respuesta: revolución socialista & Respuesta: Nuevas teorías económicas intervencionistas \\ +Estado toma control de la economía & Estado interviene para corregir mercado \\ +Planificación centralizada & Mantención de mercado con regulación \\ +Colectivización forzada & Keynesianismo y New Deal \\ +\bottomrule +\end{tabular} +\caption{Comparación de las dos grandes crisis del siglo XX} +\end{table} + +\section{Glosario de Términos Técnicos} + +\begin{description}[style=multiline, leftmargin=3cm, font=\bfseries] + +\item[Proletariado] Clase trabajadora industrial que carece de medios de producción y debe vender su fuerza de trabajo. + +\item[Socialdemocracia] Corriente política que busca acceder al poder por vía democrática para implementar reformas sociales y económicas graduales. + +\item[Estado de Bienestar] Forma de Estado que garantiza servicios sociales y protección a los ciudadanos (salud, educación, seguridad social). + +\item[Estado Intervencionista] Estado que interviene activamente en la economía para proteger la producción nacional y regular el mercado. + +\item[Comunismo de Guerra] Primera etapa de la Revolución Rusa caracterizada por la expropiación de tierras y empresas, y nacionalización de la banca. + +\item[NEP] Nueva Política Económica implementada por Lenin que otorgó libertad de mercado al sector agrícola para reactivar la economía. + +\item[Colectivización] Proceso de reunir las tierras privadas en granjas estatales, eliminando la propiedad privada de la tierra. + +\item[Koljoses] Granjas colectivas en la Unión Soviética donde los campesinos trabajaban la tierra en cooperativa. + +\item[Sovjoses] Granjas estatales en la Unión Soviética administradas directamente por el Estado. + +\item[Soviets] Consejos de delegados obreros y campesinos que surgieron durante la Revolución Rusa. + +\item[Socialismo marxista] Sistema económico basado en la propiedad social de los medios de producción y la abolición de la propiedad privada. + +\item[Keynesianismo] Teoría económica desarrollada por John Maynard Keynes que aboga por la intervención del Estado para regular los ciclos económicos. + +\item[New Deal] Conjunto de políticas implementadas por Franklin D. Roosevelt para combatir la Gran Depresión en Estados Unidos. + +\item[Taylorismo] Sistema de organización científica del trabajo desarrollado por Frederick Taylor que maximiza la eficiencia productiva. + +\item[Fordismo] Sistema de producción en cadena desarrollado por Henry Ford que combina producción masiva con salarios altos para consumo masivo. + +\end{description} + +\section{Conclusiones} + +\subsection{Lecciones de la Revolución Rusa} + +\begin{enumerate} +\item La revolución marxista no se dio en un país industrializado, sino en uno agrario, contradiciendo las predicciones de Marx. +\item Las tres etapas (Comunismo de Guerra, NEP, Colectivización) muestran las tensiones entre teoría y práctica. +\item El costo humano de la industrialización forzada fue enorme. +\item La burocratización del Estado capturó la revolución, creando una nueva élite. +\end{enumerate} + +\subsection{Lecciones de la Gran Depresión} + +\begin{enumerate} +\item Las políticas monetarias y comerciales de una potencia tienen efectos globales. +\item La especulación financiera desacoplada de la producción real genera burbujas insostenibles. +\item La economía basada en el crédito es vulnerable a pánicos y corridas. +\item El colapso del sistema financiero se transmite rápidamente a la economía real. +\end{enumerate} + +\subsection{Impacto en la teoría económica} + +Ambos eventos llevaron al desarrollo de nuevas teorías económicas: +\begin{itemize} +\item La planificación centralizada como alternativa al mercado +\item El keynesianismo y la macroeconomía moderna +\item El Estado de bienestar como estabilizador social +\item La regulación financiera como prevención de crisis +\end{itemize} + +\end{document} diff --git a/latex/imperio_romano.tex b/latex/imperio_romano.tex new file mode 100644 index 0000000..00228f0 --- /dev/null +++ b/latex/imperio_romano.tex @@ -0,0 +1,1089 @@ +\documentclass[12pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{tikz} +\usetikzlibrary{shapes.geometric,arrows,positioning,fit,calc} +\usepackage{tcolorbox} +\usepackage{booktabs} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{array} +\usepackage{longtable} +\usepackage{enumitem} +\usepackage{titlesec} +\usepackage{fancyhdr} +\usepackage{hyperref} + +\geometry{ + left=2.5cm, + right=2.5cm, + top=3cm, + bottom=3cm +} + +\setlength{\headheight}{14.5pt} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small Historia Antigua} +\fancyhead[R]{\small\thepage} +\fancyfoot[C]{\small El Imperio Romano} + +\titleformat{\section} + {\normalfont\Large\bfseries}{\thesection}{1em}{} +\titleformat{\subsection} + {\normalfont\large\bfseries}{\thesubsection}{1em}{} +\titleformat{\subsubsection} + {\normalfont\normalsize\bfseries}{\thesubsubsection}{1em}{} + +\newtcolorbox{definicion}[1][]{ + colback=blue!5!white, + colframe=blue!75!black, + fonttitle=\bfseries, + title=#1, + arc=2mm, + boxrule=1mm +} + +\newtcolorbox{ejemplo}[1][]{ + colback=green!5!white, + colframe=green!50!black, + fonttitle=\bfseries, + title=#1, + arc=2mm, + boxrule=1mm +} + +\newtcolorbox{importante}[1][]{ + colback=red!5!white, + colframe=red!75!black, + fonttitle=\bfseries, + title=#1, + arc=2mm, + boxrule=1mm +} + +\newtcolorbox{cronologia}[1][]{ + colback=yellow!5!white, + colframe=orange!75!black, + fonttitle=\bfseries, + title=#1, + arc=2mm, + boxrule=1mm +} + +\title{\textbf{\Large El Imperio Romano: Origen, Apogeo y Caída}} +\author{\textbf{Historia Antigua}\\Civilizaciones Clásicas} +\date{\textbf{Febrero 2026}} + +\begin{document} + +\maketitle +\thispagestyle{empty} + +\vspace{2cm} + +\begin{center} +\large\textbf{Tema:} La Roma Imperial - Tres continentes bajo un solo poder\\[1em] +\small Basado en la transcripción de clase y material bibliográfico de apoyo +\end{center} + +\vfill + +\begin{center} +\rule{0.5\textwidth}{0.4pt} +\end{center} + +\clearpage + +\tableofcontents + +\clearpage + +\section{Introducción} + +\subsection{Contextualización histórica} + +El Imperio Romano representa el tercer y más extenso periodo de la antigua civilización romana, sucediendo a la Monarquía (753-509 a.C.) y a la República (509-27 a.C.). Esta civilización no solo constituyó el Estado más grande de la Antigüedad clásica, sino que también sentó las bases del mundo occidental moderno en términos de derecho, arquitectura, lenguaje, organización política y militar. + +\begin{importante}[Importancia histórica] +El Imperio Romano duró aproximadamente 1480 años en total, si consideramos tanto el Imperio de Occidente (27 a.C.-476 d.C.) como el de Oriente o Bizantino (395-1453 d.C.). Durante cinco siglos, el Imperio unificado mantuvo la \textit{Pax Romana}, un periodo de relativa paz y estabilidad que permitió un comercio y cultural sin precedentes en el Mediterráneo. +\end{importante} + +\subsection{Justificación del estudio} + +El estudio del Imperio Romano es fundamental para comprender: +\begin{itemize}[leftmargin=*,itemsep=0.5em] + \item La evolución de los sistemas políticos desde la república al imperio + \item El desarrollo del derecho romano como base de los sistemas legales contemporáneos + \item La organización militar como instrumento de expansión y control territorial + \item La estructura social jerárquica y sus implicaciones económicas y políticas + \item Los procesos de integración cultural y romanización en territorios diverso + \item Las causas del declive y caída de los imperios premodernos +\end{itemize} + +\section{Cronología y Periodización} + +\subsection{Las etapas de la civilización romana} + +La historia de Roma se divide convencionalmente en tres grandes periodos: + +\begin{table}[h] +\centering +\caption{Periodos de la civilización romana} +\begin{tabular}{@{}lllc@{}} +\toprule +\textbf{Periodo} & \textbf{Inicio} & \textbf{Fin} & \textbf{Duración} \\ \midrule +Monarquía & 753 a.C. & 509 a.C. & 244 años \\ +República & 509 a.C. & 27 a.C. & 482 años \\ +Imperio & 27 a.C. & 476 d.C. (Occ.) & 503 años \\ +Imperio Bizantino & 395 d.C. & 1453 d.C. & 1058 años \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Hitos fundamentales del periodo imperial} + +\begin{cronologia}[Cronología imperial] +\begin{description}[style=multiline,leftmargin=3cm] + \item[27 a.C.] Octavio recibe el título de \textit{Augustus} y se inicia el Imperio + \item[14 d.C.] Muerte de Augusto y sucesión de Tiberio + \item[117 d.C.] Máxima extensión territorial bajo Trajano + \item[284-305 d.C.] Reformas de Diocleciano (tetrarquía) + \item[313 d.C.] Edicto de Milán (Constantino, libertad religiosa) + \item[330 d.C.] Fundación de Constantinopla + \item[395 d.C.] División definitiva del Imperio en Oriente y Occidente + \item[410 d.C.] Saco de Roma por los visigodos de Alarico + \item[476 d.C.] Deposición de Rómulo Augúlo, fin del Imperio de Occidente + \item[1453 d.C.] Caída de Constantinopla, fin del Imperio Bizantino +\end{description} +\end{cronologia} + +\subsection{Representación gráfica de la línea temporal} + +\begin{center} +\begin{tikzpicture}[scale=0.9] + \draw[->,thick] (0,0) -- (14,0) node[right] {Años}; + \foreach \x/\label in { + 0/800 a.C., + 2/600 a.C., + 4/400 a.C., + 6/200 a.C., + 8/0, + 10/200 d.C., + 12/400 d.C., + 14/600 d.C. + } { + \draw (\x,0.1) -- (\x,-0.1) node[below,font=\tiny] {\label}; + } + + \draw[red,ultra thick] (0.9,-0.5) rectangle (2.6,0.5); + \node[above,font=\tiny,red] at (1.75,0.5) {Monarquía}; + + \draw[blue,ultra thick] (2.6,-0.5) rectangle (8.4,0.5); + \node[above,font=\tiny,blue] at (5.5,0.5) {República}; + + \draw[green!60!black,ultra thick] (8.4,-0.8) rectangle (12.9,0.8); + \node[above,font=\tiny,green!60!black] at (10.65,0.8) {Imperio}; + + \draw[orange,ultra thick] (12.9,-0.5) rectangle (14,0.5); + \node[above,font=\tiny,orange] at (13.45,0.5) {Bizancio}; + + \draw[dotted,thick] (12.9,0.8) -- (14,0.8); + + \node[below,font=\tiny] at (8.4,-1) {27 a.C.}; + \node[below,font=\tiny] at (12.9,-1) {476 d.C.}; +\end{tikzpicture} +\end{center} + +\section{El Nacimiento del Imperio} + +\subsection{El contexto de la crisis republicana} + +La República romana entró en una crisis profunda durante el siglo I a.C., caracterizada por: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Conflictos civiles:} Las guerras entre Mario y Sila, y luego entre César y Pompeyo + \item \textbf{Expansionismo problemático:} Las conquistas crearon tensiones sociales y económicas + \item \textbf{Corrupción política:} El Senado perdió legitimidad y eficacia + \item \textbf{Desigualdades sociales:} La concentración de tierras en pocos latifundistas + \item \textbf{Crisis militar:} Las legiones se volvieron leales a sus generales, no al Estado +\end{itemize} + +\subsection{Augusto: El primer emperador} + +Octavio, sobrino nieto y heredero de Julio César, emergió victorioso de las guerras civiles tras derrotar a Marco Antonio y Cleopatra en la batalla de Actium (31 a.C.). + +\begin{definicion}[Augusto - Título y significado] +El título \textit{Augustus} (del latín \textit{augere}, "aumentar" o "incrementar") fue concedido a Octaviano por el Senado en el año 27 a.C. Este título, de connotaciones religiosas, significaba "el venerado" o "el que posee el \textit{augurium}". Difería del término \textit{imperator} (comandante victorioso) al conferir una autoridad sagrada y permanente más allá del simple poder militar. +\end{definicion} + +\subsection{El sistema del Principado} + +Augusto estableció un sistema político ingenioso que, formalmente, restauraba la República pero, en la práctica, concentraba todo el poder en una sola persona: + +\begin{table}[h] +\centering +\caption{Poderes de Augusto bajo el Principado} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Poder/ Título} & \textbf{Funciones} \\ \midrule +\textit{Imperium proconsulare} & Mando supremo sobre todas las provincias y ejércitos \\ +\textit{Tribunicia potestas} & Derecho de veto, inviolabilidad, convocatoria del Senado \\ +\textit{Pontifex maximus} & Jefe supremo de la religión romana \\ +\textit{Princeps senatus} & Primer miembro del Senado, fijaba el orden del día \\ +\textit{Pater patriae} & Padre de la patria (título honorífico, 2 a.C.) \\ \bottomrule +\end{tabular} +\end{table} + +\begin{importante}[La ficción republicana] +Augusto mantuvo las formas republicanas: el Senado continuó reuniéndose, los magistrados eran elegidos anualmente y las leyes se promulgaban en nombre del pueblo romano. Sin embargo, Augusto controlaba todos los centros de poder reales, especialmente el ejército, que le garantizaba la obediencia del Senado y del pueblo. +\end{importante} + +\subsection{Diagrama del sistema de gobierno bajo Augusto} + +\begin{center} +\begin{tikzpicture}[ + node distance=1.5cm, + box/.style={rectangle,draw,fill=blue!10,thick,minimum width=2.5cm,minimum height=1cm,align=center}, + emperor/.style={ellipse,draw,fill=red!20,thick,minimum width=3cm,align=center}, + arrow/.style={->,thick,>=stealth} +] + \node[emperor] (augusto) {\textbf{Augustus}\\Princeps}; + \node[box,below left=of agosto] (senado) {Senado\\(formalmente)}; + \node[box,below right=of agosto] (pueblo) {Pueblo romano\\(Comicios)}; + \node[box,below=of senado,xshift=-1.5cm] (magistrados) {Magistrados\\(Cónsules, pretores)}; + \node[box,below=of pueblo,xshift=1.5cm] (ejercito) {Ejército\\(30 legiones)}; + \node[box,below=of agosto,minimum width=6cm] (admin) {Administración provincial\\(Procuradores y legados)}; + + \draw[arrow] (augusto) -- (senado); + \draw[arrow] (augusto) -- (pueblo); + \draw[arrow] (augusto) -- (admin); + \draw[arrow,dashed] (senado) -- (magistrados); + \draw[arrow,dashed] (pueblo) -- (magistrados); + \draw[arrow,ultra thick] (augusto) -- (ejercito) node[midway,right] {\tiny Control real}; +\end{tikzpicture} +\end{center} + +\section{Organización Territorial} + +\subsection{Extensión máxima del Imperio} + +Bajo el emperador Trajano (98-117 d.C.), el Imperio alcanzó su máxima extensión histórica, abarcando aproximadamente: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Superficie total:} Entre 4 y 5 millones de kilómetros cuadrados + \item \textbf{Población estimada:} 60 a 80 millones de habitantes (aprox. el 20-25\% de la población mundial) + \item \textbf{Provincias:} Más de 40 provincias administrativas + \item \textbf{Fronteras:} Desde el norte de Bretaña hasta el desierto del Sahara, y desde la Atlántida hasta el Éufrates +\end{itemize} + +\subsection{División en tres continentes} + +El Imperio dominaba territorios en tres continentes: + +\begin{table}[h] +\centering +\caption{Territorios imperiales por continente} +\begin{tabular}{@{}lp{9cm}@{}} +\toprule +\textbf{Continente} & \textbf{Principales provincias y territorios} \\ \midrule +\textbf{Europa} & Britannia, Gallia (Lugdunensis, Aquitania, Narbonensis), Hispania (Tarraconensis, Baetica, Lusitania), Italia, Germania Superior/Inferior, Dacia, Pannonia, Noricum, Raetia, Moesia, Tracia, Macedonia, Grecia (Achaea) \\ +\textbf{África} & Aegyptus, Africa Proconsularis (Cartago), Numidia, Mauretania (Caesariensis, Tingitana), Cyrenaica, Creta et Cyrene \\ +\textbf{Asia} & Syria, Judaea (Palestina), Arabia Petraea, Mesopotamia, Armenia, Cappadocia, Galatia, Asia (Turquía occidental), Bithynia et Pontus, Cilicia, Cyprus \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Organización provincial} + +Las provincias se clasificaban en dos categorías: + +\begin{definicion}[Provincias imperiales y senatoriales] +Las \textbf{provincias imperiales} (como Britannia, Germania, Syria, Aegyptus) eran estratégicas militarmente o fronterizas. Eran gobernadas por \textit{legati Augusti} (legados del emperador) nombrados directamente por el emperador y mantenían guarniciones permanentes del ejército romano. + +Las \textbf{provincias senatoriales} (como Asia, Africa Proconsularis, Grecia) eran consideradas pacificadas. Eran gobernadas por procónsules elegidos por el Senado romano, generalmente ex-magistrados de rango consular. No albergaban tropas permanentes, salvo para mantenimiento del orden interno. +\end{definicion} + +\begin{table}[h] +\centering +\caption{Comparación entre provincias imperiales y senatoriales} +\begin{tabular}{@{}lll@{}} +\toprule +\textbf{Característica} & \textbf{Provincias imperiales} & \textbf{Provincias senatoriales} \\ \midrule +\textbf{Nombramiento} & Por el emperador & Por el Senado \\ +\textbf{Gobernador} & Legatus Augusti & Proconsul \\ +\textbf{Ejército} & Guarniciones permanentes & Sin tropas permanentes \\ +\textbf{Función} & Defensa, control fronterizo & Administración civil \\ +\textbf{Ejemplos} & Britannia, Syria, Aegyptus & Asia, Africa, Achaea \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Mapa conceptual del Imperio} + +\begin{center} +\begin{tikzpicture}[ + scale=0.7, + continent/.style={circle,draw,ultra thick,minimum size=3cm,align=center,font=\large\bfseries}, + region/.style={rectangle,draw,fill=white,minimum width=1.5cm,minimum height=0.6cm,font=\tiny} +] + \node[continent,fill=blue!20] (europa) at (0,3) {Europa}; + \node[continent,fill=yellow!20] (asia) at (4,3) {Asia}; + \node[continent,fill=green!20] (africa) at (2,-1) {África}; + + \node[region,above=0.1cm of europa] {Britannia}; + \node[region,above left=0.1cm and 0.5cm of europa] {Germania}; + \node[region,left=0.1cm of europa] {Gallia}; + \node[region,below left=0.1cm and 0.3cm of europa] {Hispania}; + \node[region,below=0.1cm of europa] {Italia}; + \node[region,below right=0.1cm and 0.3cm of europa] {Dacia}; + \node[region,right=0.1cm of europa] {Grecia}; + + \node[region,above=0.1cm of asia] {Armenia}; + \node[region,above left=0.1cm and 0.3cm of asia] {Mesopotamia}; + \node[region,left=0.1cm of asia] {Syria}; + \node[region,below left=0.1cm and 0.3cm of asia] {Judaea}; + \node[region,below=0.1cm of asia] {Asia Minor}; + + \node[region,above=0.1cm of africa] {Aegyptus}; + \node[region,above left=0.1cm and 0.4cm of africa] {Cyrenaica}; + \node[region,above right=0.1cm and 0.4cm of africa] {Africa}; + \node[region,right=0.1cm of africa] {Numidia}; + \node[region,left=0.1cm of africa] {Mauretania}; + + \node[star,star points=5,fill=red,minimum size=1cm] at (1.5,1.3) {}; + \node[below,font=\tiny] at (1.5,1.3) {ROMA}; + + \draw[thick,dotted] (-2,-2) rectangle (6,5); + \node[above right,font=\small] at (-2,5) {Mediterraneum Mare}; +\end{tikzpicture} +\end{center} + +\section{Organización Militar} + +\subsection{El ejército romano como pilar del poder} + +El ejército romano constituyó el verdadero instrumento del poder imperial. No solo garantizaba la defensa del territorio, sino que también servía como herramienta de control político, motor de la economía y vía de romanización de las provincias. + +\begin{importante}[El papel político del ejército] +La lealtad de las legiones determinaba la legitimidad del emperador. Numerosos emperadores fueron proclamados por sus tropas, y la amenaza de rebelión militar condicionaba constantemente las decisiones políticas. La famosa expresión \textit{possidebant soldier imperium} ("los soldados poseían el imperio") resume esta realidad. +\end{importante} + +\subsection{Estructura y composición del ejército imperial} + +\subsubsection{Las legiones} + +La unidad fundamental del ejército romano era la legión, una fuerza de infantería pesada altamente organizada y profesional. + +\begin{definicion}[La legión imperial] +La legión romana era una unidad militar permanente de ciudadanos romanos, compuesta por aproximadamente 5.300 hombres. Cada legión era autónoma, con su propia numeración, nombre y símbolo (aquila). Las legiones estaban diseñadas para operar de forma independiente durante largos periodos, contando con especialistas, ingenieros, artilleros y personal administrativo. +\end{definicion} + +\subsection{Composición de una legión} + +Una legión típica del Alto Imperio se organizaba de la siguiente manera: + +\begin{table}[h] +\centering +\caption{Estructura interna de una legión} +\begin{tabular}{@{}lccp{6cm}@{}} +\toprule +\textbf{Unidad} & \textbf{Hombres} & \textbf{Comandante} & \textbf{Descripción} \\ \midrule +Legión & 5.280 & \textit{Legatus legionis} & Unidad completa \\ +Cohorte & 480 & \textit{Tribunus cohortis} & 10 por legión \\ +Siglo & 80 & \textit{Centurio} & 60 por legión \\ +\textit{Contubernium} & 8 & \textit{Decanus} & 660 por legión \\ +Cuerpos auxiliares & 120 & \textit{Praefectus} & Caballería (120 hombres) \\ +\textit{Numerus} & 500-1000 & \textit{Praefectus} & Tropas aliadas/auxiliares \\ \bottomrule +\end{tabular} +\end{table} + +\subsubsection{Cálculo del tamaño del ejército imperial} + +El número total de efectivos militares se puede calcular mediante la fórmula: + +\begin{equation} +N_{total} = (L \times N_L) + (C \times N_C) + (A \times N_A) +\end{equation} + +Donde: +\begin{itemize} + \item $N_{total}$ = número total de soldados + \item $L$ = número de legiones (30 durante el Alto Imperio) + \item $N_L$ = efectivos por legión (5.300 hombres) + \item $C$ = número de cohortes auxiliares (aprox. 120) + \item $N_C$ = efectivos por cohorte auxiliar (500 hombres) + \item $A$ = unidades especiales (pretorianos, caballería, flota) + \item $N_A$ = efectivos de unidades especiales (variable) +\end{itemize} + +\begin{ejemplo}[Cálculo de efectivos durante el Alto Imperio] +Para el periodo de máximo esplendor (siglo I-II d.C.): + +\begin{align*} +\text{Legionarios: } & 30 \times 5.300 = 159.000 \text{ hombres} \\ +\text{Auxilia: } & 120 \times 500 = 60.000 \text{ hombres} \\ +\text{Guardia Pretoriana: } & 9.000 \text{ hombres} \\ +\text{Marina: } & 30.000 \text{ hombres} \\ +\hline +\textbf{TOTAL: } & \mathbf{258.000 \text{ hombres}} +\end{align*} + +Esta cifra representa aproximadamente el 0.4\% de la población total del Imperio (60 millones), una proporción razonable para un Estado preindustrial. +\end{ejemplo} + +\subsection{Las tropas auxiliares} + +Las \textit{auxilia} (tropas auxiliares) complementaban a las legiones y estaban compuestas por \textit{peregrini} (súbditos no ciudadanos). + +\begin{table}[h] +\centering +\caption{Comparación entre legionarios y auxilia} +\begin{tabular}{@{}lll@{}} +\toprule +\textbf{Característica} & \textbf{Legionarios} & \textbf{Auxilia} \\ \midrule +\textbf{Estatus legal} & Ciudadanos romanos & No ciudadanos \\ +\textbf{Armamento} & Armadura pesada, pilum, gladius & Equipamiento variado (arco, flecha, caballería) \\ +\textbf{Función} & Infantería pesada, combate principal & Apoyo, caballería, hostigamiento \\ +\textbf{Reclutamiento} & Principalmente Italia y provincias romanas & Provincias fronterizas, aliados \\ +\textbf{Duración servicio} & 20-25 años & 25 años \\ +\textbf{Recompensa} & Ciudadanía (si no la tenían) o tierra & Ciudadanía y tierra al licenciarse \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Jerarquía y carrera militar} + +\begin{table}[h] +\centering +\caption{Jerarquía de oficiales en una legión} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Rango} & \textbf{Responsabilidades} \\ \midrule +\textit{Legatus Augusti} & Comandante de la legión, nombrado por el emperador, senador \\ +\textit{Tribunus laticlavius} & Segundo comandante, senador en inicio de carrera \\ +\textit{Praefectus castrorum} & Tercer comandante, oficial de carrera (centurión más antiguo) \\ +\textit{Tribuni angusticlavii} & Cinco tribunos menores, caballeros (equites) \\ +\textit{Primus pilus} & Centurión más antiguo de la legión, máximo rango de centurión \\ +\textit{Centuriones} & Comandantes de siglos (60 hombres cada uno), 60 por legión \\ +\textit{Optiones} & Segundos al mando de los centuriones \\ +\textit{Decani} & Comandantes de \textit{contubernia} (grupos de 8 hombres) \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Diagrama organizacional del ejército} + +\begin{center} +\begin{tikzpicture}[ + node distance=0.8cm, + rank/.style={rectangle,draw,fill=blue!20,minimum width=4cm,align=center,font=\small}, + unit/.style={rectangle,draw,fill=green!10,minimum width=3cm,align=center,font=\footnotesize}, + arrow/.style={->,thick,>=stealth} +] + \node[rank,fill=red!20] (imperador) {\textbf{IMPERATOR}\\Sumo comandante militar}; + \node[rank,below left=of imperador] (legado) {\textit{Legatus Augusti}\\Comandante de legión}; + \node[rank,below right=of imperador] (prefecto) {\textit{Praefectus}\\Comandante auxilia}; + \node[unit,below=of legado] (centuriones) {60 Centuriones\\(comandantes de siglos)}; + \node[unit,below=of prefecto] (decuriones) {Decuriones\\(caballería)}; + + \draw[arrow] (imperador) -- (legado); + \draw[arrow] (imperador) -- (prefecto); + \draw[arrow] (legado) -- (centuriones); + \draw[arrow] (prefecto) -- (decuriones); + + \node[below=0.5cm of centuriones,font=\tiny] (soldados1) {5.280 legionarios}; + \node[below=0.5cm of decuriones,font=\tiny] (soldados2) {120-500 auxiliares}; +\end{tikzpicture} +\end{center} + +\subsection{Las fronteras militares} + +Las fronteras del Imperio (\textit{limes}) estaban fortificadas y guarnecidas permanentemente: + +\begin{table}[h] +\centering +\caption{Principales fronteras del Imperio} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Frontera} & \textbf{Defensas y guarniciones} \\ \midrule +\textbf{Britannia} & Muro de Adriano (122 d.C.), Muro de Antonino (142 d.C.), 3 legiones \\ +\textbf{Germania} & Fronteras del Rin y Danubio, 8 legiones, fortalezas (\textit{castella}) \\ +\textbf{Dacia} & Frontera natural de los Cárpatos, 2 legiones \\ +\textbf{Oriente} & Frontera del Éufrates, 7 legiones, defensa contra partos y sasánidas \\ +\textbf{África} & Foso y muro en el sur, defensa contra tribus del desierto, 1 legión \\ \bottomrule +\end{tabular} +\end{table} + +\section{Estructura Social} + +\subsection{La jerarquía social romana} + +La sociedad romana era profundamente jerárquica y estratificada, con una clara división entre libres y no libres, y entre los libres, entre ciudadanos y no ciudadanos. + +\begin{definicion}[Ordos: las clases sociales romanas] +La sociedad romana se organizaba formalmente en \textit{ordines} (órdenes o clases), estamentos jurídicos con derechos, obligaciones y privilegios diferenciados. El acceso a los ordines superiores estaba condicionado por el censo (riqueza), el nacimiento y, progresivamente, el favor imperial. +\end{definicion} + +\subsection{Los tres grandes estratos sociales} + +\subsubsection{Patricios y aristocracia} + +El término "patricio" durante el Imperio evolucionó desde su significado republicano original: + +\begin{table}[h] +\centering +\caption{Aristocracia imperial: senatorial y ecuestre} +\begin{tabular}{@{}llp{7cm}@{}} +\toprule +\textbf{Ordo} & \textbf{Censo mínimo} & \textbf{Privilegios y funciones} \\ \midrule +\textit{Ordo senatorius} & 1.000.000 sestercios & Miembros del Senado, acceso a magistraturas, gobierno de provincias imperiales, cargos militares (legados), prohibición de comercio, vestimenta (toga con banda púrpura), asientos especiales en espectáculos \\ +\textit{Ordo equestris} & 400.000 sestercios & Comandancia de auxilia, procuradurías, cargos administrativos, permitido comercio y banca, vestimenta (toga con banda angosta púrpura), anillo aureo \\ +\textit{Nobilis} & Variable & Nobiliado hereditario, miembros de familias senatoriales antiguas, distinción honorífica mas no jurídica formal \\ \bottomrule +\end{tabular} +\end{table} + +\begin{importante}[La aristocracia patricia] +La clase senatorial durante el Alto Imperio incluía aproximadamente 600 familias, con un Senado de 600 miembros. Los senadores poseían inmensas fortunas, generalmente en tierras (latifundia) en Italia y las provincias. La mayoría vivía en Roma o en sus villas suburbanas, manteniendo un estilo de vida lujoso y cultivando las artes y las letras. +\end{importante} + +\subsubsection{Plebeyos} + +La plebe constituía la gran mayoría de la población libre y se subdividía en múltiples categorías: + +\begin{table}[h] +\centering +\caption{Categorías de plebeyos} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Categoría} & \textbf{Características} \\ \midrule +\textit{Plebs urbana} & Habitantes de Roma, masas urbanas dependientes del \textit{annona} (subsidio de grano), clientes de patrones, trabajadores informales, Participaban en espectáculos y manifestaciones políticas \\ +\textit{Plebs rustica} & Campesinos libres, pequeños propietarios, arrendatarios, coloni (colonos) \\ +\textit{Veterani} & Soldados licenciados con tierras o pensiones (\textit{praemia}), importantes para colonización y romanización de provincias \\ +\textit{Liberti} (libertos) & Esclavos manumitidos, obligados de \textit{obsequium} (deber de respeto) hacia el antiguo amo (\textit{patronus}), limitaciones políticas (no podían acceder al Senado), pero plena capacidad económica \\ +\textit{Peregrini} & Habitantes libres de provincias sin ciudadanía romana, pagaban tributo (\textit{tributum}), no podían servir en legiones (solo auxilia), localmente gobernados por sus propias élites indígenas \\ \bottomrule +\end{tabular} +\end{table} + +\begin{ejemplo}[El \textit{cursus publicus}] +Los plebeyos urbanos de Roma recibían regularmente el \textit{annona}, una distribución gratuita o subvencionada de grano (más tarde también aceite y vino). Esta política, iniciada por la República y ampliada por los emperadores, garantizaba la lealtad de las masas urbanas ("pan y circo": \textit{panem et circenses}). En el siglo II d.C., se estima que 200.000-300.000 personas recibían el annona en Roma. +\end{ejemplo} + +\subsubsection{Esclavos} + +La esclavitud era una institución fundamental y omnipresente en la sociedad romana: + +\begin{definicion}[Estatus jurídico de la esclavitud] +El esclavo (\textit{servus}) era una "cosa" (\textit{res}) según el derecho romano, sin capacidad jurídica propia. El esclavo era propiedad de su amo (\textit{dominus}), quien tenía \textit{potestas} (poder) sobre su vida, muerte y castigo. Sin embargo, el esclavo podía ser manumitido (liberado), adquiriendo así la condición de liberto. +\end{definicion} + +\begin{table}[h] +\centering +\caption{Categorías de esclavos} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Categoría} & \textbf{Descripción} \\ \midrule +\textit{Servi publici} & Esclavos del Estado (pública), trabajos en obras públicas, oficinas gubernamentales, minas públicas, relativamente protegidos por ley \\ +\textit{Servi privati} & Esclavos de particulares, mayoría de esclavos, condición dependía del amo \\ +\textit{Vernae} & Esclavos nacidos en casa del amo, generalmente mejor tratados, potencial de manumisión \\ +\textit{Esclavos urbanos} & Domésticos, artesanos, comerciantes, tutores, escribas, contables, médicos, mayor oportunidad de manumisión \\ +\textit{Esclavos rurales} & Trabajadores en latifundios, minas, condiciones más duras, menor oportunidad de libertad \\ +\textit{Gladiadores} & Entrenados para luchar en espectáculos, posición ambigua (celebrados pero despreciados) \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Dinámica social y movilidad} + +\subsubsection{La manumisión como mecanismo de integración} + +La manumisión (liberación de esclavos) era un importante mecanismo de movilidad social y renovación de la ciudadanía: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Manumisión \textit{vindicta}:} Ceremonia judicial con látiz (\textit{vindicta}) + \item \textbf{Manumisión \textit{censu}:} Inscripción en el censo por el magistrado + \item \textbf{Manumisión \textit{testamento}:} Disposición en el testamento del amo + \item \textbf{Manumisión \textit{inter amicos}:} Declaración informal entre amigos + \item \textbf{Manumisión \textit{in ecclesia}:} Dentro de una iglesia (periodo cristiano) +\end{itemize} + +\subsubsection{El cliente y el \textit{patronus}} + +El sistema de clientela (\textit{clientela}) era una institución fundamental que estructuraba las relaciones sociales: + +\begin{definicion}[La clientela romana] +La \textit{clientela} era una relación de reciprocidad asimétrica y hereditaria entre un \textit{patronus} (patrón, generalmente un aristócrata) y un \textit{cliens} (cliente, plebeyo). El patrono proporcionaba protección jurídica, apoyo económico y regalos (\textit{sportulae}); el cliente ofrecía lealtad política, acompañamiento público y votos. +\end{definicion} + +\subsection{Diagrama de la estructura social} + +\begin{center} +\begin{tikzpicture}[ + scale=0.85, + estrato/.style={rectangle,draw,thick,minimum width=11cm,minimum height=1.2cm,align=left,font=\normalsize}, + arrow/.style={<->,thick} +] + \node[estrato,fill=red!20] (senatorial) { + \textbf{Ordo Senatorius} (1.000.000+ sestercios) --- Senado, magistraturas, gobiernos provinciales + }; + \node[estrato,fill=orange!20,below=0.3cm of senatorial] (ecuestre) { + \textbf{Ordo Equestris} (400.000+ sestercios) --- Comandancia auxilia, procuradurías, comercios + }; + \node[estrato,fill=yellow!20,below=0.3cm of ecuestre] (plebe1) { + \textbf{Plebs urbana} --- Masas urbanas, \textit{annona}, clientes + }; + \node[estrato,fill=yellow!10,below=0.3cm of plebe1] (plebe2) { + \textbf{Plebs rustica, veterans} --- Campesinos, soldados licenciados, pequeños propietarios + }; + \node[estrato,fill=green!10,below=0.3cm of plebe2] (liberti) { + \textbf{Liberti} --- Esclavos manumitidos, obligaciones hacia el \textit{patronus} + }; + \node[estrato,fill=gray!20,below=0.3cm of liberti] (peregrini) { + \textbf{Peregrini} --- Habitantes libres no ciudadanos, provinciales + }; + \node[estrato,fill=gray!40,below=0.3cm of peregrini] (esclavos) { + \textbf{Servi} --- Esclavos (urbano, rural, público, privado) + }; + + \node[right=0.5cm of senatorial,font=\tiny,align=left] {~1\% de población}; + \node[right=0.5cm of ecuestre,font=\tiny,align=left] {~2-3\%}; + \node[right=0.5cm of plebe1,font=\tiny,align=left] {~10-15\%}; + \node[right=0.5cm of plebe2,font=\tiny,align=left] {~20-25\%}; + \node[right=0.5cm of liberti,font=\tiny,align=left] {~5-10\%}; + \node[right=0.5cm of peregrini,font=\tiny,align=left] {~30-40\%}; + \node[right=0.5cm of esclavos,font=\tiny,align=left] {~15-20\%}; +\end{tikzpicture} +\end{center} + +\section{Economía y Sociedad} + +\subsection{Base económica del Imperio} + +La economía romana era fundamentalmente agraria, con agricultura como principal actividad productiva: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Agricultura:} 80-90\% de la población trabajaba en el campo + \item \textbf{Latifundia:} Grandes estates propiedad de aristócratas, trabajados por esclavos o coloni + \item \textbf{Villas:} Explotaciones agrícolas de tamaño medio, típicas de Italia + \item \textbf{Colonias:} Asentamientos de veteranos, importantes para romanización +\end{itemize} + +\subsection{Comercio e integración económica} + +\begin{importante}[La \textit{Pax Romana} y el comercio] +El periodo de la \textit{Pax Romana} (27 a.C.-180 d.C.) permitió un comercio sin precedentes en el Mediterráneo. Las rutas comerciales conectaban Britannia (estaño) con Egipto (grano), Hispania (aceite, vino, metales) con Oriente (especias, seda, perfumes). El Mediterráneo se convirtió en un "lago romano" (\textit{Mare Nostrum}), con seguridad y navegación regular. +\end{importante} + +\begin{table}[h] +\centering +\caption{Principales productos comerciales} +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Región} & \textbf{Productos exportados} \\ \midrule +Egipto & Grano, papiro, vidrio, lino \\ +Hispania & Aceite, vino, metales (oro, plata, cobre), garum \\ +Gallia & Vino, cerámica, productos agrícolas \\ +África & Grano, aceite, cerámica, piedras preciosas \\ +Oriente & Especias, seda, perfumes, marfil \\ +Britannia & Estaño, plomo, oro, lana, esclavos \\ +Germania & Ámbar, pieles, esclavos, ganado \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Moneda y fiscalidad} + +\begin{table}[h] +\centering +\caption{Sistema monetario romano (Alto Imperio)} +\begin{tabular}{@{}lccp{6cm}@{}} +\toprule +\textbf{Moneda} & \textbf{Metal} & \textbf{Valor} & \textbf{Uso} \\ \midrule +\textit{Aureus} & Oro & 25 denarii & Grandes transacciones, donativa \\ +\textit{Denarius} & Plata & 1 & Moneda estándar, salarios del ejército \\ +\textit{Sestertius} & Bronce & 1/4 denarius & Comercio cotidiano \\ +\textit{Dupondius} & Bronce & 1/2 sestertius & Pequeñas transacciones \\ +\textit{As} & Cobre & 1/4 sestertius & Compras mínimas \\ \bottomrule +\end{tabular} +\end{table} + +\section{Cultura y Romanización} + +\subsection{El proceso de romanización} + +La romanización fue el proceso cultural por el cual las provincias adoptaron las costumbres, lengua, derecho y religion romanas: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Urbanismo:} Fundación de ciudades con foro, templo, teatro, termas, acueducto + \item \textbf{Lengua latina:} Administración, comercio, derecho, literatura + \item \textbf{Derecho romano:} Sistema legal uniforme, ciudadanía progresiva + \item \textbf{Religión:} Culto imperial sincretismo con religiones locales + \item \textbf{Ejército:} Servicio militar como vía de integración + \item \textbf{Matrimonios mixtos:} Romanos con locales, creando élites romanizadas +\end{itemize} + +\subsection{La ciudad romana como instrumento de integración} + +\begin{definicion}[El municipio romano] +El \textit{municipium} era una comunidad con autonomía local, gobernada por magistrados locales (\textit{duoviri}, \textit{aediles}, \textit{quaestores}), pero bajo la autoridad romana. Los habitantes podían tener distintos estatus jurídicos: ciudadanía completa (\textit{civitas optimo jure}), derecho latino (\textit{latinitium}), o simplemente súbditos (\textit{peregrini}). Las ciudades eran el núcleo de la administración provincial y la romanización. +\end{definicion} + +\begin{table}[h] +\centering +\caption{Estructura urbana romana típica} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Elemento} & \textbf{Función} \\ \midrule +\textit{Forum} & Plaza central, mercado, reunión política, judicial \\ +\textit{Capitoliu/Basilica} & Templo de Júpiter / Tribunal y admin. \\ +\textit{Theatrum, Amphitheatrum} & Espectáculos, propaganda imperial \\ +\textit{Thermae} & Baños públicos, espacio social \\ +\textit{Aquae ductus} & Suministro de agua, ingeniería \\ +\textit{Viae} & Calles empedradas, comunicación \\ +\textit{Domus/Insulae} & Residencias de élite / bloques de apartamentos \\ +\textit{Anfiteatro} & Ludi gladiatorii, caza de animales \\ \bottomrule +\end{tabular} +\end{table} + +\section{La División del Imperio} + +\subsection{Causas de la división} + +La decisión de dividir el Imperio en dos mitades respondió a múltiples factores: + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Dificultad de administración:} Territorio extenso para un solo gobierno + \item \textbf{Presiones militares:} Fronteras múltiples y simultáneas (germanos, partos, sasánidas) + \item \textbf{Diferencias culturales:} Oriente helenístico vs. Occidente latino + \item \textbf{Rivalidades dinásticas:} Emperadores rivales reclamando el poder + \item \textbf{Reformas administrativas:} Diocleciano (tetrarquía) y Constantino +\end{itemize} + +\subsection{La Tetrarquía de Diocleciano (284-305 d.C.)} + +\begin{definicion}[Tetrarquía] +La tetrarquía ("gobierno de cuatro") fue un sistema instituido por Diocleciano en 293 d.C. que dividía el Imperio en dos partes, cada una gobernada por un \textit{augustus} (emperador principal) asistido por un \textit{caesar} (cesar, emperador junior y sucesor designado). Este sistema buscaba solucionar los problemas de sucesión y defensa mediante una división funcional del poder. +\end{definicion} + +\begin{table}[h] +\centering +\caption{La primera tetrarquía (293-305 d.C.)} +\begin{tabular}{@{}llcc@{}} +\toprule +\textbf{Título} & \textbf{Nombre} & \textbf{Capital} & \textbf{Territorio} \\ \midrule +\textit{Augustus} Oriente & Diocleciano & Nicomedia & Oriente, Egipto, Tracia, Asia \\ +\textit{Caesar} Oriente & Galerio & Sirmium & Iliria, Grecia, Balcanes \\ +\textit{Augustus} Occidente & Maximiano & Milán & Italia, África, Hispania, Gallia \\ +\textit{Caesar} Occidente & Constancio Cloro & Tréveris & Britannia, Germania \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{La división definitiva de 395 d.C.} + +Tras la muerte de Teodosio I el Grande, el Imperio se dividió definitivamente entre sus dos hijos: + +\begin{table}[h] +\centering +\caption{Imperio de Oriente vs. Imperio de Occidente (395 d.C.)} +\begin{tabular}{@{}p{5cm}p{5cm}@{}} +\toprule +\textbf{Imperio de Oriente} & \textbf{Imperio de Occidente} \\ \midrule +Emperador: Arcadio & Emperador: Honorio \\ +Capital: Constantinopla & Capital: Milán (después Ravena) \\ +Territorio más rico y poblado & Territorio más pobre y despoblado \\ +Cultura helenística & Cultura latina \\ +Economía más fuerte & Economía más débil \\ +Ciudades grandes y ricas & Ciudades en declive \\ +Fronteras más defendibles & Fronteras muy extensas y vulnerables \\ +Duró hasta 1453 d.C. & Cayó en 476 d.C. \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Diagrama de la división imperial} + +\begin{center} +\begin{tikzpicture}[ + scale=0.7, + empire/.style={ellipse,draw,ultra thick,minimum width=4cm,align=center}, + region/.style={rectangle,draw,fill=white,font=\tiny} +] + \node[empire,fill=blue!20] (oriente) at (6,3) {\textbf{ORIENTE}\\(Imperium Romanum Orientis)\\Constantinopla}; + \node[empire,fill=red!20] (occidente) at (0,3) {\textbf{OCCIDENTE}\\(Imperium Romanum Occidentis)\\Mediolanum/Ravenna}; + + \node[region] at (-2,5) {Britannia}; + \node[region] at (-2,3.5) {Gallia}; + \node[region] at (-2,2) {Hispania}; + \node[region] at (-2,0.5) {Africa Proconsularis}; + \node[region] at (-0.5,0.5) {Italia}; + + \node[region] at (4,5) {Thracia}; + \node[region] at (4,3.5) {Asia Minor}; + \node[region] at (4,2) {Syria}; + \node[region] at (6,5) {Aegyptus}; + \node[region] at (7.5,2) {Mesopotamia}; + \node[region] at (6,0.5) {Graecia}; + + \draw[->,thick] (2,3) -- (4,3) node[midway,above,font=\tiny] {División 395 d.C.}; +\end{tikzpicture} +\end{center} + +\section{El Declive y Caída del Imperio de Occidente} + +\subsection{Causas del declive} + +La caída del Imperio de Occidente fue un proceso complejo y multifactorial: + +\begin{table}[h] +\centering +\caption{Causas del declive del Imperio de Occidente} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Categoría} & \textbf{Factores específicos} \\ \midrule +\textbf{Militares} & Presiones bárbaras en fronteras, ejército barbarizado, debilidad del reclutamiento romano, dependencia de foederati (aliados bárbaros) \\ +\textbf{Económicos} & Disminución del comercio, inflación, devaluación del denarius, concentración de tierras, abandono de villas, crisis del annona \\ +\textbf{Políticos} & Inestabilidad imperial, emperadores títeres, usurpaciones, guerras civiles, pérdida de autoridad central, decadencia del Senado \\ +\textbf{Sociales} & Declive de las ciudades, huida de campos (\textit{colonato}), ruralización, declive de la clase media \\ +\textbf{Culturales} & Pérdida de \textit{virtus} romana tradicional, cristianización, cambios de valores \\ +\textbf{Externos} & Invasiones hunas (pression on Germania), migraciones germánicas (visigodos, vándalos, suevos, alanos), fortalecimiento del Imperio Sasánida \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Cronología del declive} + +\begin{cronologia}[Eventos clave del declive] +\begin{description}[style=multiline,leftmargin=3cm] + \item[235-284 d.C.] Crisis del siglo III: anarquía militar, emperadores efímeros, invasiones, peste + \item[378 d.C.] Batalla de Adrianópolis: muerte del emperador Valente frente a los godos + \item[410 d.C.] Saco de Roma por los visigodos de Alarico + \item[429-439 d.C.] Invasión vándala de África (pérdida del granero de Roma) + \item[451-452 d.C.] Invasión de Atila y los hunos + \item[455 d.C.] Saco de Roma por los vándalos de Genserico + \item[476 d.C.] Deposition of Rómulo Augúulus por Odoacro, fin del Imperio de Occidente +\end{description} +\end{cronologia} + +\subsection{El evento de 476 d.C.} + +\begin{importante}[El fin del Imperio de Occidente] +En septiembre de 476 d.C., el caudillo germano Odoacro depuso al último emperador de Occidente, Rómulo Augúulus (un niño de unos 10 años), y envió las insignias imperiales a Constantinopla, reconociendo (al menos nominalmente) la autoridad del emperador de Oriente, Zenón. Este evento, consagrado por la tradición historiográfica desde el siglo XVIII, simboliza el fin del Imperio Romano de Occidente, aunque la caída fue un proceso gradual y los romano orientales nunca aceptaron permanentemente esta pérdida. +\end{importante} + +\subsection{Supervivencia del Imperio de Oriente} + +\begin{definicion}[Imperio Bizantino] +El Imperio Romano de Oriente, conocido modernamente como Imperio Bizantino (del nombre de la antigua Bizancio, rebautizada Constantinopla en 330 d.C.), continuó como Estado romano cristiano hasta 1453 d.C., con una cultura predominantemente griega pero que se consideraba plenamente romana (\textit{Rhomaioi}). Bizancio preservó y transmitió la cultura clásica, desarrolló una arte original, mantuvo una economía sofisticada y sirvió como buffer contra la expansión islámica. +\end{definicion} + +\section{Legado del Imperio Romano} + +\subsection{Aportes a la civilización occidental} + +\begin{table}[h] +\centering +\caption{Legado romano en diversos ámbitos} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Ámbito} & \textbf{Contribuciones} \\ \midrule +\textbf{Derecho} & Derecho romano como base del derecho civil continental, conceptos de jurisprudencia, \textit{habeas corpus}, presunción de inocencia, propiedad privada \\ +\textbf{Lengua} & Latín como raíz de lenguas romances (español, portugués, francés, italiano, rumano), latín como lengua académica y eclesiástica \\ +\textbf{Arquitectura} & Arco, bóveda, cúpula, concreto (\textit{opus caementicium}), acueductos, termas, calzadas (\textit{viae}), anfiteatros \\ +\textbf{Ingeniería} & Sistemas de suministro de agua, alcantarillado, puentes, puertos, técnicas de construcción \\ +\textbf{Literatura} & Géneros (epopeya, historia, oratoria, poesía), autores (Virgilio, Cicerón, Tito Livio, Ovidio) \\ +\textbf{Política} & Conceptos de república, senado, ciudadanía, administración provincial, burocracia profesional \\ +\textbf{Calendario} & Calendario juliano (45 a.C.), base del calendario gregoriano \\ +\textbf{Religión} & Cristianismo como religión imperial (Edicto de Tesalónica, 380 d.C.), difusión por todo el Imperio \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Conceptos fundamentales del derecho romano} + +\begin{definicion}[Principios del derecho romano] +\begin{enumerate}[label=\roman*.] + \item \textbf{\textit{Ius civile}:} Derecho propio de los ciudadanos romanos, exclusivo para quienes tenían la \textit{civitas} + \item \textbf{\textit{Ius gentium}:} Derecho de gentes, aplicable a todos (romanos y no romanos), basado en la razón natural (\textit{ratio naturalis}) + \item \textbf{\textit{Ius naturale}:} Derecho natural, universal e inmutable, basado en la naturaleza humana + \item \textbf{\textit{Aequitas}:} Equidad, justicia como adaptación del derecho estricto a circunstancias específicas + \item \textbf{\textit{Ratio scripta}:} Razón escrita, la jurisprudencia y codificación del derecho +\end{enumerate} +\end{definicion} + +\subsection{La ciudadanía romana como concepto evolutivo} + +\begin{table}[h] +\centering +\caption{Evolución de la ciudadanía romana} +\begin{tabular}{@{}lp{8cm}@{}} +\toprule +\textbf{Etapa/Ley} & \textbf{Extensión de la ciudadanía} \\ \midrule +\textit{Lex Julia} (90 a.C.) & Concedida a todos los italianos al sur del río Po (tras la Guerra Social) \\ +\textit{Lex Plautia Papiria} (89 a.C.) & Extendida a la Galia Cispadana (norte de Italia) \\ +\textit{Constitutio Antoniniana} (212 d.C.) & Caracalla concede ciudadanía a todos los habitantes libres del Imperio \\ +\textit{Resultado final} & Ciudadanía convertida de privilegio exclusivo a estatus casi universal, perdiendo parte de su valor exclusivo pero unificando jurídicamente el Imperio \\ \bottomrule +\end{tabular} +\end{table} + +\section*{Glosario} + +\begin{description}[style=multiline,leftmargin=3cm] + + \item[Aediles] Magistrados romanos encargados del mantenimiento de edificios públicos, mercados, y organización de festivales + + \item[\textit{Annona}] Distribución gratuita o subvencionada de grano a la población de Roma + + \item[\textit{Aquaeductus}] Sistema de ingeniería para transportar agua desde fuentes distantes a las ciudades + + \item[\textit{Auxilia}] Tropas auxiliares del ejército romano compuestas por no ciudadanos + + \item[\textit{Aureus}] Moneda de oro romana, valorada en 25 denarii + + \item[\textit{Castrum}] Campamento militar romano fortificado, origen de muchas ciudades europeas + + \item[\textit{Civitas}] Ciudadanía romana; también comunidad urbana con autonomía local + + \item[\textit{Clientela}] Sistema patrono-cliente de relaciones de reciprocidad asimétrica + + \item[\textit{Cohors}] Unidad militar de 480 hombres, décima parte de una legión + + \item[\textit{Colonus}] Colono, campesino que trabajaba tierras de un terrateniente + + \item[\textit{Comitium}] Lugar de reunión de las asambleas romanas + + \item[\textit{Contubernium}] Grupo de 8 soldados que compartían tienda en el campamento + + \item[\textit{Decanus}] Comandante de un \textit{contubernium} + + \item[\textit{Denarius}] Moneda de plata romana, estándar del sistema monetario + + \item[\textit{Domus}] Casa residencial de familias ricas, con patio central (\textit{atrium}) + + \item[\textit{Duoviri}] Magistrados principales de un municipio + + \item[\textit{Equites} (Ordo equestris)] Orden ecuestre, clase de caballeros, segunda en rango tras los senadores + + \item[\textit{Foederati}] Pueblos bárbaros aliados del Imperio mediante tratado (\textit{foedus}) + + \item[\textit{Forum}] Plaza central de la ciudad romana, centro político, comercial y judicial + + \item[\textit{Gladius}] Espada corta romana, arma principal del legionario + + \item[\textit{Hiberna}] Cuartel de invierno del ejército romano + + \item[\textit{Imperator}] Comandante victorioso, título que deviene en "emperador" + + \item[\textit{Imperium}] Poder de mando, especialmente militar y judicial + + \item[\textit{Insulae}] Bloques de apartamentos populares en ciudades romanas + + \item[\textit{Ius civile}] Derecho civil romano, exclusivo para ciudadanos + + \item[\textit{Ius gentium}] Derecho de gentes, aplicable a romanos y extranjeros + + \item[\textit{Latifundium}] Gran propiedad territorial trabajada por esclavos o colonos + + \item[\textit{Latinitium}] Derecho latino, estatus intermedio entre no ciudadanía y ciudadanía plena + + \item[\textit{Legatus} (legatus Augusti)] Comandante de una legión, nombrado por el emperador + + \item[\textit{Legio}] Legión, unidad militar fundamental de 5.300 hombres + + \item[\textit{Limes}] Frontera fortificada del Imperio + + \item[\textit{Magistratus}] Magistrado romano, cargo electo con poder (\textit{potestas}) + + \item[\textit{Manumissio}] Manumisión, liberación formal de un esclavo + + \item[\textit{Mare Nostrum}] "Nuestro mar", el Mediterráneo bajo control romano + + \item[\textit{Municipium}] Municipio, ciudad con autonomía local bajo autoridad romana + + \item[\textit{Novus homo}] "Hombre nuevo", primer miembro de una familia en alcanzar el Senado + + \item[\textit{Opus caementicium}] Concreto romano, material de construcción revolucionario + + \item[\textit{Ordo}] Orden, clase social jurídica (senatorial, ecuestre) + + \item[\textit{Pax Romana}] Paz romana, periodo de estabilidad (27 a.C.-180 d.C.) + + \item[\textit{Peregrinus}] Habitante libre no ciudadano del Imperio + + \item[\textit{Pilum}] Jabalina romana, arma arrojadiza del legionario + + \item[\textit{Pontifex maximus}] Sumo pontífice, jefe de la religión romana + + \item[\textit{Praefectus}] Prefecto, oficial de rango elevado en el ejército o administración + + \item[\textit{Praetor}] Pretor, magistrado judicial, inferior en rango a los cónsules + + \item[\textit{Princeps}] "Primero", título de Augusto significando "primer ciudadano" + + \item[\textit{Procurator}] Procurador, administrador financiero de una provincia imperial + + \item[\textit{Provincia}] Provincia, territorio fuera de Italia administrado por Roma + + \item[\textit{Quaestor}] Cuestor, magistrado encargado de finanzas y administración + + \item[\textit{Res publica}] "Cosa pública", la república como bien común + + \item[\textit{Sestertius}] Moneda de bronce, cuarta parte de un denarius + + \item[\textit{Sportulae}] Pequeñas regalos monetarios dados por patrones a clientes + + \item[\textit{Tabularium}] Archivo de documentos estatales + + \item[\textit{Testamentum}] Testamento, disposición legal de bienes tras la muerte + + \item[\textit{Tribuni angusticlavii}] Tribunos de la franja angosta, oficiales ecuestres en la legión + + \item[\textit{Tribuni militum}] Tribunos militares, oficiales superiores en la legión + + \item[\textit{Tribunus plebis}] Tribuno de la plebe, magistrado con veto e inviolabilidad + + \item[\textit{Via}] Calle o camino romano, vía de comunicación pavimentada + + \item[\textit{Villa}] Finca rústica, propiedad rural de una familia rica + +\end{description} + +\section*{Referencias Bibliográficas} + +\subsection*{Fuentes primarias} + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Suetonio,} \textit{De Vita Caesarum} (Vidas de los doce césares), siglo II d.C. + \item \textbf{Tácito,} \textit{Annales} y \textit{Historiae}, siglos I-II d.C. + \item \textbf{Tito Livio,} \textit{Ab Urbe Condita} (Desde la fundación de la ciudad), siglo I a.C. + \item \textbf{Casiodoro,} \textit{Variae} (colección de documentos oficiales), siglo VI d.C. + \item \textbf{Eutropio,} \textit{Breviarium historiae Romanae} (Compendio de historia romana), siglo IV d.C. +\end{itemize} + +\subsection*{Fuentes secundarias} + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Gibbon, Edward.} \textit{The History of the Decline and Fall of the Roman Empire.} Londres, 1776-1789. 6 vols. + \item \textbf{Mommsen, Theodor.} \textit{Römisches Staatsrecht} (Derecho público romano). Leipzig, 1871-1888. 3 vols. + \item \textbf{Rostovtzeff, Michael.} \textit{The Social and Economic History of the Roman Empire.} Oxford, Clarendon Press, 1926. 2 vols. + \item \textbf{Syme, Ronald.} \textit{The Roman Revolution.} Oxford, Clarendon Press, 1939. + \item \textbf{Alföldy, Géza.} \textit{Römische Sozialgeschichte.} Wiesbaden, Steiner, 1975. + \item \textbf{Millar, Fergus.} \textit{The Emperor in the Roman World.} Londres, Duckworth, 1977. + \item \textbf{Hopkins, Keith.} \textit{Conquerors and Slaves.} Cambridge, Cambridge University Press, 1978. + \item \textbf{Ward-Perkins, Bryan.} \textit{The Fall of Rome and the End of Civilization.} Oxford, Oxford University Press, 2005. + \item \textbf{Heather, Peter.} \textit{The Fall of the Roman Empire: A New History of Rome and the Barbarians.} Oxford, Oxford University Press, 2005. + \item \textbf{Goldsworthy, Adrian.} \textit{In the Name of Rome: The Men Who Won the Roman Empire.} Londres, Weidenfeld \& Nicolson, 2003. +\end{itemize} + +\subsection*{Bibliografía especializada} + +\begin{itemize}[leftmargin=*,itemsep=0.3em] + \item \textbf{Le Bohec, Yann.} \textit{L'armée romaine sous le Haut-Empire.} París, Picard, 1989. + \item \textbf{Scheidel, Walter (ed.).} \textit{The Cambridge Companion to the Roman Economy.} Cambridge, Cambridge University Press, 2012. + \item \textbf{Erdkamp, Paul.} \textit{The Grain Market in the Roman Empire.} Cambridge, Cambridge University Press, 2005. + \item \textbf{Veyne, Paul (ed.).} \textit{Historia de la vida privada.} Vol. 1: \textit{Del Imperio romano al año mil.} Madrid, Taurus, 1987. + \item \textbf{Ando, Clifford.} \textit{Imperial Ideology and Provincial Loyalty in the Roman Empire.} Berkeley, University of California Press, 2000. + \item \textbf{Kelly, Christopher.} \textit{Ruling the Later Roman Empire.} Cambridge, MA, Harvard University Press, 2004. + \item \textbf{Harl, Kenneth W.} \textit{Coinage in the Roman Economy, 300 B.C. to A.D. 700.} Baltimore, Johns Hopkins University Press, 1996. +\end{itemize} + +\vspace{1cm} + +\begin{center} +\rule{0.3\textwidth}{0.4pt}\\[0.5em] +\small \textit{Fin del documento}\\[0.3em] +\rule{0.3\textwidth}{0.4pt} +\end{center} + +\end{document} diff --git a/latex/resumen.md b/latex/resumen.md new file mode 100644 index 0000000..ff5c115 --- /dev/null +++ b/latex/resumen.md @@ -0,0 +1,221 @@ +# Prompt para Generar Resúmenes Académicos en LaTeX + +## Instrucciones de Uso + +1. Transcribir la clase (audio a texto) usando Whisper o similar +2. Tener el material bibliográfico en formato digital (PDF escaneado con OCR o texto) +3. Copiar el prompt de abajo y completar los campos entre `[corchetes]` + +--- + +## Prompt Template + +``` +Sos un asistente académico experto en [MATERIA]. Tu tarea es crear un resumen extenso y detallado en LaTeX basado en la transcripción de clase y el material bibliográfico que te proporciono. + +## Material de entrada + +### Transcripción de clase: +[PEGAR TRANSCRIPCIÓN AQUÍ] + +### Material bibliográfico de apoyo: +[PEGAR TEXTO DEL LIBRO/APUNTE O INDICAR QUE LO SUBISTE COMO ARCHIVO] + +## Requisitos del resumen + +### Extensión y profundidad: +- Mínimo 10 páginas +- Cubrir TODOS los temas mencionados en clase +- Expandir cada concepto con definiciones formales del material bibliográfico +- No resumir demasiado: preferir explicaciones completas + +### Estructura obligatoria: +1. Portada con título, materia, fecha y tema +2. Índice (table of contents) +3. Introducción contextualizando el tema +4. Desarrollo organizado en secciones y subsecciones +5. Tablas comparativas cuando haya clasificaciones o tipos +6. Diagramas con TikZ cuando haya procesos, flujos o relaciones +7. Cajas destacadas para definiciones, ejemplos y conceptos importantes +8. Fórmulas matemáticas cuando corresponda +9. Glosario de términos técnicos al final +10. Referencias al material bibliográfico + +### Formato LaTeX requerido: + +```latex +\documentclass[11pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[spanish,provide=*]{babel} +\usepackage{amsmath,amssymb} +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,shapes.geometric,calc} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{fancyhdr} +\usepackage{titlesec} +\usepackage{tcolorbox} +\usepackage{array} +\usepackage{multirow} + +\geometry{margin=2.5cm} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{[MATERIA] - CBC} +\fancyhead[R]{Clase [N]} +\fancyfoot[C]{\thepage} + +% Cajas para destacar contenido +\newtcolorbox{definicion}[1][]{ + colback=blue!5!white, + colframe=blue!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{importante}[1][]{ + colback=red!5!white, + colframe=red!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{ejemplo}[1][]{ + colback=green!5!white, + colframe=green!50!black, + fonttitle=\bfseries, + title=#1 +} +``` + +### Estilo de contenido: +- Usar \textbf{} para términos clave en su primera aparición +- Usar \textit{} para énfasis y palabras en otros idiomas +- Incluir ejemplos concretos mencionados en clase +- Relacionar teoría con casos prácticos +- Mantener el tono académico pero accesible +- Si el profesor hizo énfasis en algo ("esto es importante", "esto entra en el parcial"), destacarlo en caja roja + +### Elementos visuales: +- Tablas con booktabs para comparaciones (usar \toprule, \midrule, \bottomrule) +- Diagramas TikZ para flujos, ciclos o relaciones entre conceptos +- Listas itemize/enumerate para secuencias o características +- Fórmulas centradas con equation o align para expresiones matemáticas + +## Ejemplo de calidad esperada + +Para cada concepto principal: +1. Definición formal (del libro) +2. Explicación en palabras simples (como lo explicó el profesor) +3. Ejemplo concreto +4. Relación con otros conceptos +5. Por qué es importante / para qué sirve + +## Output + +Generá el archivo .tex completo, listo para compilar con pdflatex (dos pasadas para el índice). +``` + +--- + +## Comandos para compilar + +```bash +# Compilar (dos veces para índice) +pdflatex resumen_clase_X.tex +pdflatex resumen_clase_X.tex + +# Abrir PDF +xdg-open resumen_clase_X.pdf # Linux +open resumen_clase_X.pdf # macOS +``` + +--- + +## Pipeline completo + +### 1. Transcripción de audio (con Whisper) + +```bash +# Instalar whisper +pip install openai-whisper + +# Transcribir audio de clase +whisper "clase_X.mp3" --language Spanish --output_format txt +``` + +### 2. OCR de PDFs escaneados (con marker-pdf) + +```bash +# Crear entorno virtual +python -m venv .venv +source .venv/bin/activate + +# Instalar marker +pip install marker-pdf + +# Procesar PDF (usa GPU si está disponible) +marker_single "libro_capitulo_X.pdf" --output_dir output/ +``` + +### 3. Generar resumen + +Usar el prompt de arriba con: +- Claude (Anthropic) +- GPT-4 (OpenAI) +- Gemini (Google) + +### 4. Compilar LaTeX + +```bash +pdflatex resumen.tex && pdflatex resumen.tex +``` + +--- + +## Tips para mejores resultados + +1. **Transcripción completa**: No cortar la transcripción, la IA necesita todo el contexto +2. **Material bibliográfico**: Incluir los capítulos específicos, no todo el libro +3. **Ser específico**: Indicar la materia, el nivel (CBC, carrera, posgrado) y el enfoque del profesor +4. **Iterar**: Si el primer resultado es corto, pedir "expandí la sección X con más detalle" +5. **Diagramas**: Si hay un diagrama importante, describirlo y pedir que lo haga en TikZ +6. **Revisar**: La IA puede cometer errores conceptuales, siempre verificar con el material + +--- + +## Materias donde funciona bien + +- Economía (micro/macro) +- Física +- Química +- Matemática (álgebra, análisis) +- Biología +- Sociología +- Historia +- Derecho (con adaptaciones) +- Cualquier materia con contenido teórico estructurado + +--- + +## Ejemplo de uso rápido + +``` +Sos un asistente académico experto en Física. Creá un resumen extenso en LaTeX sobre "Cinemática" basado en esta transcripción de clase del CBC: + +[pegar transcripción] + +Material de apoyo: Capítulo 2 de Serway "Movimiento en una dimensión": + +[pegar texto del capítulo] + +Incluí: +- Definiciones de posición, velocidad, aceleración +- Fórmulas del MRU y MRUV +- Diagramas de movimiento con TikZ +- Gráficos posición-tiempo y velocidad-tiempo +- Ejemplos resueltos paso a paso +- Glosario de términos +``` diff --git a/main.py b/main.py index c03a839..088f4d6 100644 --- a/main.py +++ b/main.py @@ -407,9 +407,11 @@ def run_main_loop() -> None: # Step 3: Generate AI summary and documents telegram_service.send_message( - f"🤖 Generando resumen con IA..." + f"🤖 Generando resumen académico LaTeX..." + ) + doc_generator = DocumentGenerator( + notification_callback=lambda msg: telegram_service.send_message(msg) ) - doc_generator = DocumentGenerator() success, summary, output_files = ( doc_generator.generate_summary( transcription_text, base_name @@ -428,13 +430,13 @@ def run_main_loop() -> None: except Exception: pass + # Upload all files in parallel using batch upload + upload_tasks = [] + # Upload transcription TXT if transcription_file.exists(): remote_txt = f"{settings.RESUMENES_FOLDER}/{transcription_file.name}" - webdav_service.upload( - transcription_file, remote_txt - ) - logger.info(f"Uploaded: {remote_txt}") + upload_tasks.append((transcription_file, remote_txt)) # Upload DOCX docx_path = Path( @@ -442,10 +444,7 @@ def run_main_loop() -> None: ) if docx_path.exists(): remote_docx = f"{settings.DOCX_FOLDER}/{docx_path.name}" - webdav_service.upload( - docx_path, remote_docx - ) - logger.info(f"Uploaded: {remote_docx}") + upload_tasks.append((docx_path, remote_docx)) # Upload PDF pdf_path = Path( @@ -453,8 +452,7 @@ def run_main_loop() -> None: ) if pdf_path.exists(): remote_pdf = f"{settings.DOCX_FOLDER}/{pdf_path.name}" - webdav_service.upload(pdf_path, remote_pdf) - logger.info(f"Uploaded: {remote_pdf}") + upload_tasks.append((pdf_path, remote_pdf)) # Upload Markdown md_path = Path( @@ -462,8 +460,14 @@ def run_main_loop() -> None: ) if md_path.exists(): remote_md = f"{settings.RESUMENES_FOLDER}/{md_path.name}" - webdav_service.upload(md_path, remote_md) - logger.info(f"Uploaded: {remote_md}") + upload_tasks.append((md_path, remote_md)) + + # Execute parallel uploads + if upload_tasks: + upload_results = webdav_service.upload_batch( + upload_tasks, max_workers=4, timeout=120 + ) + logger.info(f"Parallel upload complete: {len(upload_results)} files") # Final notification telegram_service.send_message( diff --git a/resumen_curiosidades.tex b/resumen_curiosidades.tex new file mode 100644 index 0000000..79739ed --- /dev/null +++ b/resumen_curiosidades.tex @@ -0,0 +1,953 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[spanish,provide=*]{babel} +\usepackage{amsmath,amssymb} +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,shapes.geometric,calc,shapes.misc} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{fancyhdr} +\usepackage{titlesec} +\usepackage{tcolorbox} +\usepackage{array} +\usepackage{multirow} +\usepackage{csquotes} +\usepackage{pgfplots} +\pgfplotsset{compat=1.18} + +\geometry{margin=2.5cm} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{Curiosidades Científicas y Culturales} +\fancyhead[R]{Compilación Interdisciplinaria} +\fancyfoot[C]{\thepage} + +% Cajas para destacar contenido +\newtcolorbox{definicion}[1][]{ + colback=blue!5!white, + colframe=blue!75!black, + fonttitle=\bfseries, + title=#1, + sharp corners=downhill +} + +\newtcolorbox{importante}[1][]{ + colback=red!5!white, + colframe=red!75!black, + fonttitle=\bfseries, + title=#1, + sharp corners=downhill +} + +\newtcolorbox{ejemplo}[1][]{ + colback=green!5!white, + colframe=green!50!black, + fonttitle=\bfseries, + title=#1, + sharp corners=downhill +} + +\newtcolorbox{dato}[1][]{ + colback=yellow!5!white, + colframe=orange!75!black, + fonttitle=\bfseries, + title=#1, + sharp corners=downhill +} + +\newtcolorbox{formula}[1][]{ + colback=purple!5!white, + colframe=purple!75!black, + fonttitle=\bfseries, + title=#1, + sharp corners=downhill +} + +\title{\textbf{25 Cosas que No Sabías Hace Cinco Minutos}\\[0.5cm] +\large{Un compendio interdisciplinario de curiosidades científicas,\\fenómenos culturales y avances tecnológicos}} +\author{Compilación Académica} +\date{\today} + +\begin{document} + +\maketitle +\thispagestyle{empty} + +\tableofcontents +\newpage + +\section{Introducción} + +El conocimiento humano se caracteriza por su naturaleza fragmentada y especializada. Sin embargo, algunas de las comprensiones más valiosas surgen precisamente de la \textbf{interconexión entre disciplinas} aparentemente dispares. El presente documento compila veinticinco curiosidades que abarcan desde la gastronomía francesa hasta la bioacústica marina, desde la psicología del consumidor hasta la ingeniería de materiales, demostrando que el curiosity-driven learning --el aprendizaje impulsado por la curiosidad-- representa una de las formas más efectivas de adquirir conocimiento interconectado. + +\begin{importante}[Enfoque Interdisciplinario] +Este documento está organizado para mostrar conexiones entre áreas tradicionalmente separadas del conocimiento. Cada curiosidad sirve como punto de entrada para explorar conceptos más profundos en física, biología, psicología, ingeniería y cultura. +\end{importante} + +\section{Gastronomía y Cultura Alimentaria} + +\subsection{El Hot Dog Francés: Adaptación Culinaría Transcultural} + +\begin{definicion}[Hot Dog Francés] +Variante gastronómica del hot dog tradicional estadounidense que incorpora técnicas de panadería francesa y métodos de preparación distintivos. +\end{definicion} + +\textbf{Diferenciación técnica con el hot dog tradicional:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}p{5cm}@{}p{5cm}@{}} +\toprule +\textbf{Hot Dog Tradicional} & \textbf{Hot Dog Francés} \\ \midrule +Pan blando de forma alargada & Baguette crujiente \\ +Corte longitudinal del pan & Perforación central del pan \\ +Salsas aplicadas externamente & Salsas inyectadas internamente \\ +Queso cheddar o americano & Emmental o Gruyère rallado \\ +Ensamblaje lineal & Técnica de relleno tubular \\ \bottomrule +\end{tabular} +\caption{Comparación técnica entre hot dog tradicional y francés} +\end{table} + +\textbf{Proceso de preparación:} + +\begin{enumerate} + \item \textbf{Selección del pan}: Baguette fresca, crujiente externamente, suave internamente + \item \textbf{Perforación}: Se crea un túnel central utilizando utensilio especializado + \item \textbf{Inyección de salsas}: Mayonesa, mostaza Dijon o variaciones regionales + \item \textbf{Inserción de salchicha}: Generalmente salchicha tipo Toulouse o similar + \item \textbf{Aplicación de queso}: Emmental o Gruyère rallado, parcialmente derretido +\end{enumerate} + +\begin{ejemplo}[Contexto Cultural] +Este plato ilustra el concepto de \textit{glocalización} --adaptación de productos globales a preferencias locales-- donde el concepto estadounidense de hot dog se hibrida con la tradición panadera francesa de la baguette, creando un producto único que mantiene elementos de ambas culturas. +\end{ejemplo} + +\subsection{Heinz Tomato Ketchup Smoothie: Innovación y Controversia} + +\begin{dato}[Heinz Tomato Ketchup Smoothie] +Bebida desarrollada por Heinz que combina ketchup con ingredientes frutales, destacando la naturaleza botánica del tomate como fruta. +\end{dato} + +\textbf{Composición y análisis:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Ingrediente} & \textbf{Función en la Bebida} \\ \midrule +Ketchup & Base, sabor ácido-dulce característico \\ +Sorbete de açaí & Textura, antioxidantes, color púrpura \\ +Jugo de manzana & Dulzor natural, líquido base \\ +Fresas & Sabor frutal complementario, color \\ +Frambuesas & Acidez, notas frutales, vitaminas \\ \bottomrule +\end{tabular} +\end{table} + +\begin{definicion}[Solanum lycopersicum] +Nombre científico del tomate, botánicamente clasificado como una baya (fruta) aunque culinariamente tratado como vegetal. Esta ambigüedad clasificatoria permite la creación de productos que desafían las categorías culinarias tradicionales. +\end{definicion} + +\textbf{Análisis desde la teoría de marketing:} + +Este producto representa una estrategia de \textbf{diferenciación por extinción} --crear productos tan inusuales que generen conversación y cobertura mediática--, convirtiendo la controversial naturaleza del producto en su principal característica de marketing. + +\section{Biología, Salud y Medicina} + +\subsection{Propiedades Odontológicas del Brócoli} + +\begin{importante}[Propiedad Antibacteriana] +El consumo regular de brócoli contribuye a la reducción de placa dental mediante la inhibición de \textit{Streptococcus mutans}, la bacteria primarily responsable de caries y enfermedad periodontal. +\end{importante} + +\textbf{Mecanismo bioquímico de acción:} + +\begin{itemize} + \item \textbf{Sulforafano}: Compuesto azufrado con propiedades antibacterianas + \item \textbf{Fibra mecánica}: Acción limpiadora abrasiva durante masticación + \item \textbf{Isothiocyanatos}: Modificación del pH bucal hacia ambientes menos favorables para bacterias cariogénicas + \item \textbf{Antioxidantes}: Protección del esmalte dental +\end{itemize} + +\begin{definicion}[Streptococcus mutans] +Bacteria grampositiva, anaerobia facultativa, considerada el principal agente etiológico de caries dental en humanos. Su capacidad para formar biopelículas (placa) y metabolizar carbohidratos produciendo ácido láctico la hace particularmente patogénica para la estructura dental. +\end{definicion} + +\textbf{Proceso de formación de placa:} + +\begin{center} +\begin{tikzpicture}[ + node distance=1.5cm, + process/.style={rectangle, draw, fill=blue!10, text width=3cm, align=center, rounded corners}, + arrow/.style={-Stealth, thick} +] + \node[process] (bacterias) {Colonización de $S. mutans$}; + \node[process, right=of bacterias] (biofilm) {Formación de biopelícula}; + \node[process, right=of biofilm] (acido) {Producción de ácido}; + \node[process, right=of acido, fill=red!10] (caries) {Demineralización del esmalte}; + + \draw[arrow] (bacterias) -- (biofilm); + \draw[arrow] (biofilm) -- (acido); + \draw[arrow] (acido) -- (caries); +\end{tikzpicture} +\end{center} + +El brócoli interfiere específicamente en la etapa de colonización, reduciendo la capacidad de $S. mutans$ de adherirse a la superficie dental y formar biopelículas cohesivas. + +\subsection{Mimetismo en el Pez Murciélago} + +\begin{definicion}[Pez Murciélago] +Pez de la familia Ogcocephalidae que en su etapa juvenil desarrolla un camuflaje pasivo imitando hojas flotantes, mecanismo evolutivo de supervivencia en etapas vulnerables del desarrollo. +\end{definicion} + +\textbf{Características del mimetismo ontogénico:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}p{5cm}@{}p{5cm}@{}} +\toprule +\textbf{Etapa Juvenil} & \textbf{Etapa Adulta} \\ \midrule +Imita hoja flotante & Forma de pez murciélago típica \\ +Comportamiento pasivo & Movimiento activo \\ +Superficie de agua libre & Aguas profundas \\ +Depredación evasiva & Depredación activa \\ +Coloración críptica & Coloración aposemática \\ \bottomrule +\end{tabular} +\caption{Dimorfismo ontogénico en pez murciélago} +\end{table} + +\begin{ejemplo}[Secuencia de Desarrollo] +\begin{enumerate} + \item \textbf{Eclosión}: Larva planctónica inicial + \item \textbf{Asentamiento}: Migración a superficie, adopción de forma de hoja + \item \textbf{Cripis}: Camuflaje pasivo, deriva con corrientes + \item \textbf{Metamorfosis}: Desarrollo de características adultas + \item \textbf{Transición}: Abandono del disfraz, migración a profundidad + \item \textbf{Madurez}: Patrón de coloración aposemática, estilo de vida bentónico +\end{enumerate} +\end{ejemplo} + +\textbf{Ventajas evolutivas del camuflaje ontogénico:} + +\begin{itemize} + \item \textbf{Reducción de depredación}: Estadísticamente significativo en etapas vulnerables + \item \textbf{Ahorro energético}: No requiere inversión en fuga o combate + \item \textbf{Aproximación a presas}: Permite acercarse sin detección + \item \textbf{Optimización de recursos}: Energía dirigida a crecimiento en lugar de defensa activa +\end{itemize} + +\section{Psicología y Economía Conductual} + +\subsection{El Efecto Señuelo: Arquitectura de Choice} + +\begin{definicion}[Efecto Señuelo / Decoy Effect] +Sesgo cognitivo donde la presencia de una tercera opción poco atractiva (el señuelo) modifica sistemáticamente las preferencias entre dos opciones principales, dirigiendo la elección hacia la opción preferida por el ofertante. +\end{definicion} + +\textbf{Formalización matemática del efecto señuelo:} + +Sean $A$, $B$ y $D$ tres opciones, donde $D$ es el señuelo diseñado para favorecer $B$ sobre $A$. El efecto señuelo ocurre cuando: + +\begin{equation} +P(B|A,B) < P(B|A,B,D) +\end{equation} + +Donde $P(B|A,B)$ es la probabilidad de elegir $B$ cuando solo están presentes $A$ y $B$, y $P(B|A,B,D)$ es la probabilidad de elegir $B$ cuando el señuelo $D$ está presente. + +\textbf{Ejemplo paradigmático - Palomitas de cine:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ccc@{}} +\toprule +\textbf{Tamaño} & \textbf{Precio} & \textbf{Precio por unidad} \\ +\midrule +Pequeña & \$4.00 & \$0.40/oz \\ +Mediana & \$6.50 & \$0.43/oz \\ +Grande & \$7.00 & \$0.28/oz \\ +\bottomrule +\end{tabular} +\caption{Estructura de precios con señuelo incorporado} +\end{table} + +\begin{importante}[Análisis del Disparidad] +La mediana sirve como señuelo porque: +\begin{itemize} + \item Precio por unidad PEOR que la pequeña + \item Solo \$0.50 menos que la grande + \item Casi nadie la compra (propósito no-consumo) + \item Hace que la grande parezca una ``ganga comparativa'' +\end{itemize} +\end{importante} + +\textbf{Fundamentos cognitivos:} + +\begin{enumerate} + \item \textbf{Comparación relativa}: Los humanos evaluamos opciones en contexto, no absolutamente + \item \textbf{Aversión a la pérdida}: No obtener el ``mejor valor'' se percibe como pérdida + \item \textbf{Anclaje}: La mediana sirve como referencia que hace la grande parecer razonable + \item \textbf{Simplificación heurística}: Elegimos la opción que requiere menos justificación cognitiva +\end{enumerate} + +\begin{tikzpicture}[ + node distance=2cm, + box/.style={rectangle, draw, minimum width=2cm, minimum height=1cm, align=center}, + decoy/.style={rectangle, draw, dashed, minimum width=2cm, minimum height=1cm, align=center} +] + \node[box, align=center] (pequena) {Pequeña\\\$4}; + \node[decoy, right=1cm of pequena, align=center] (mediana) {Mediana\\\$6.50\\(Señuelo)}; + \node[box, right=1cm of mediana, fill=green!20, align=center] (grande) {Grande\\\$7\\(Objetivo)}; + + \draw[->, thick, red] (mediana) to[bend left] node[above, font=\tiny] {hace atractiva} (grande); + \draw[->, dashed, gray] (pequena) to[bend right] node[below, font=\tiny] {menos valor percibido} (grande); +\end{tikzpicture} + +\subsection{Aplicaciones del Efecto Señuelo} + +\begin{itemize} + \item \textbf{Suscripciones de software}: Free, Pro (señuelo), Enterprise (objetivo) + \item \textbf{Productos electrónicos}: Modelos intermedios con características específicas para impulsar premium + \item \textbf{Menús de restaurantes}: Platos extremadamente caros que hacen otros parecer razonables + \item \textbf{Billetes de avión}: Clases configuradas para incentivar cierta elección + \item \textbf{Políticas públicas}: Presentación de opciones para dirigir opinión pública +\end{itemize} + +\section{Ingeniería y Tecnología} + +\subsection{Cubos de Basura Inteligentes: Visión Artificial Aplicada} + +\begin{definicion}[Sistema de Predicción de Trayectoria] +Sistema cibernético que integra visión computacional, aprendizaje automático y actuación mecánica para anticipar la trayectoria de objetos en movimiento y posicionarse óptimamente para su recepción. +\end{definicion} + +\textbf{Arquitectura del sistema HTX Studio:} + +\begin{center} +\begin{tikzpicture}[ + sensor/.style={circle, draw, fill=yellow!20, minimum size=1cm}, + process/.style={rectangle, draw, fill=blue!10, minimum width=2cm}, + actuator/.style={rectangle, draw, fill=green!10, minimum width=2cm}, + arrow/.style={-Stealth, thick} +] + \node[sensor] (camera) {Cámara}; + \node[process, right=1.5cm of camera, align=center] (vision) {Visión\\Computacional}; + \node[process, right=1cm of vision, align=center] (ml) {Machine\\Learning}; + \node[process, right=1cm of ml, align=center] (predict) {Predicción\\Trayectoria}; + \node[actuator, right=1cm of predict] (motor) {Motor}; + \node[below=0.5cm of motor] (trash) {Cubo}; + + \draw[arrow] (camera) -- (vision); + \draw[arrow] (vision) -- (ml); + \draw[arrow] (ml) -- (predict); + \draw[arrow] (predict) -- (motor); + \draw[arrow] (motor) -- (trash); +\end{tikzpicture} +\end{center} + +\textbf{Especificaciones técnicas:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Componente} & \textbf{Especificación} \\ \midrule +Sensores & Cámaras de alta velocidad (60+ fps) \\ +Procesamiento & GPU integrada para inferencia en tiempo real \\ +Algoritmo & Red neuronal convolucional para detección \\ +Actuación & Motores DC con encoder de posición \\ +Latencia & <100 ms de detección a movimiento \\ +Precisión & >90\% en condiciones normales \\ \bottomrule +\end{tabular} +\end{table} + +\subsection{Realidad Aumentada Automotriz: Sistema Xpeng} + +\begin{dato}[Emotional AR Driving] +El fabricante chino Xpeng implementa realidad aumentada para comunicación emocional entre conductores, permitiendo proyección de emojis virtuales hacia otros vehículos. +\end{dato} + +\textbf{Objetivos de diseño:} + +\begin{itemize} + \item \textbf{Canalización de agresión}: Alternativa no-física a gestos agresivos + \item \textbf{Seguridad vial}: Reducción de confrontaciones físicas + \item \textbf{Expresión emocional}: Válvula de escape para frustración + \item \textbf{Diferenciación de marca}: Característica distintiva en mercado saturado +\end{itemize} + +\textbf{Implementación técnica:} + +\begin{itemize} + \item \textbf{Hardware}: Proyectores HUD (Head-Up Display) en parabrisas + \item \textbf{Software}: Sistema de selección gestual o por comandos de voz + \item \textbf{Renderizado}: Emojis superpuestos a visión del mundo real + \item \textbf{Calibración}: Ajuste automático según distancia del vehículo objetivo +\end{itemize} + +\subsection{Repulsión Magnética: Campaña Mercedes-Benz} + +\begin{ejemplo}[Brilliant Marketing Campaign] +Mercedes-Benz desarrolló carritos de juguete con imanes de repulsión ocultos para demostrar su tecnología de frenados ABS, creando vehículos ``incolisionables'' que paradójicamente fueron rechazados por el público infantil. +\end{ejemplo} + +\textbf{Principio físico implementado:} + +\begin{formula}[Ley de Repulsión Magnética] +\begin{equation} +F = \frac{\mu q_1 q_2}{4\pi r^2} +\end{equation} +Donde $F$ es la fuerza de repulsión, $\mu$ la permeabilidad magnética, $q_1$ y $q_2$ las cargas magnéticas (polos), y $r$ la distancia entre imanes. +\end{formula} + +\textbf{Paradoja de aceptación:} + +\begin{itemize} + \item \textbf{Objetivo publicitario}: Demostrar tecnología de prevención de colisiones + \item \textbf{Resultado técnico}: Carritos efectivamente incapaces de colisionar + \item \textbf{Recepción infantil}: Rechazo universal (eliminaba la diversión principal: chocar autos) + \item \textbf{Resultado publicitario}: Campaña viral exitosa a pesar de falla comercial del producto +\end{itemize} + +Este caso ilustra la tensión entre \textit{seguridad} y \textit{ludicidad}, mostrando que en productos recreativos, la prevención del comportamiento ``peligroso'' puede eliminar el valor principal del producto. + +\subsection{Sensor Láser de Privacidad Computacional} + +\begin{definicion}[Sistema de Seguridad por Hilo Láser] +Dispositivo de seguridad informática que utiliza un haz láser invisible como sensor perimetral; cuando el haz es interrumpido por aproximación, el sistema oculta automáticamente el contenido de pantalla. +\end{definicion} + +\textbf{Especificaciones técnicas:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Parámetro} & \textbf{Especificación} \\ \midrule +Fuente láser & Diodo láser clase 1 (seguro para ojos) \\ +Longitud de onda & 650 nm (rojo invisible) \\ +Rango de detección & 0.5-2 metros \\ +Latencia de respuesta & <50 ms \\ +Integración OS & Windows/macOS/Linux \\ \bottomrule +\end{tabular} +\end{table} + +\textbf{Aplicaciones:} + +\begin{itemize} + \item Oficinas con tráfico peatonal constante + \item Espacios de trabajo compartido + \item Ambientes donde se maneja información sensible + \item Prevención de visualización accidental de contenido confidencial +\end{itemize} + +\section{Ciencia de Materiales} + +\subsection{Resistencia de Piezas Lego: Efecto del Pigmento} + +\begin{importante}[Variación por Color] +La resistencia mecánica de las piezas de Lego varía significativamente según el pigmento utilizado, con diferencias de hasta 80 kg en capacidad de carga entre el amarillo (550 kg) y el blanco (630 kg). +\end{importante} + +\textbf{Datos de resistencia por color:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}lcc@{}} +\toprule +\textbf{Color} & \textbf{Resistencia (kg)} & \textbf{Variación vs. Base} \\ \midrule +Amarillo & 550 & -12.5\% \\ +Negro & 560 & -11.1\% \\ +Azul & 565 & -10.3\% \\ +Rojo & 600 & -4.8\% \\ +Blanco & \textbf{630} & \textbf{Referencia (+0\%)} \\ \bottomrule +\end{tabular} +\caption{Capacidad de carga según pigmento (Datos experimentales)} +\end{table} + +\begin{dato}[Explicación Científica] +\begin{itemize} + \item \textbf{Pigmentos intensos} (azul): Introducen imperfecciones cristalinas en el polímero ABS + \item \textbf{Pigmento blanco} (dióxido de titanio): Actúa como filler reforzante + \item \textbf{Mecanismo}: Dióxido de titanio se integra a matriz polimérica aumentando densidad de entrecruzamiento + \item \textbf{Consecuencia}: Piezas blancas consistentemente más resistentes que otras colores +\end{itemize} +\end{dato} + +\textbf{Control de calidad Lego:} + +\begin{itemize} + \item \textbf{Tasa de defectos}: 18 piezas por millón (0.0018\%) + \item \textbf{Tolerancia dimensional}: $\pm$0.002 mm (2 micras) + \item \textbf{Materiales}: Moldes de acero inoxidable endurecido + \item \textbf{Vida útil de molde}: ~1 millón de ciclos antes de reemplazo +\end{itemize} + +\subsection{Resortera Gigante: Mecánica de Proyectiles} + +\textbf{Análisis físico del invento de Mike Shake:} + +\begin{formula}[Energía Potencial Elástica] +\begin{equation} +E_p = \frac{1}{2}kx^2 +\end{equation} +Donde $E_p$ es la energía potencial almacenada, $k$ la constante elástica del material, y $x$ la distancia de estiramiento. +\end{formula} + +\begin{formula}[Energía Cinética de Proyectil] +\begin{equation} +E_c = \frac{1}{2}mv^2 +\end{equation} +Donde $E_c$ es la energía cinética, $m$ la masa del proyectil (bola de acero), y $v$ la velocidad de salida. +\end{formula} + +\textbf{Sistema de manivela de tensión:} + +\begin{itemize} + \item \textbf{Ventaja mecánica}: Relación de transmisión >10:1 + \item \textbf{Acumulación gradual}: Energía aplicada en múltiples rotaciones + \item \textbf{Seguridad}: Reducción de riesgo de retroceso + \item \textbf{Precisión}: Control exacto del grado de tensión +\end{itemize} + +\section{Acústica y Bioacústica} + +\subsection{Propagación del Sonido en Diferentes Medios} + +\begin{definicion}[Atenuación Sonora] +Pérdida de intensidad del sonido a medida que se propaga through un medio, dependiente de las propiedades físicas del medio y la frecuencia de la onda sonora. +\end{definicion} + +\textbf{Comparación de alcances máximos:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}lcccc@{}} +\toprule +\textbf{Medio} & \textbf{Densidad} & \textbf{Animal} & \textbf{Frecuencia} & \textbf{Alcance} \\ \midrule +Aire & $1.225\text{ kg/m}^3$ & Lobo & 300-2000 Hz & 16 km \\ +Agua & $1000\text{ kg/m}^3$ & Ballena azul & 10-40 Hz & 1600+ km \\ +\bottomrule +\end{tabular} +\end{table} + +\begin{importante}[Explicación Física] +\begin{itemize} + \item \textbf{Densidad del medio}: Agua ~800 veces más densa que aire + \item \textbf{Absorción}: Menor en agua para frecuencias bajas + \item \textbf{Canal SOFAR}: Canal de sonido profundo en océano que guía ondas sonoras + \item \textbf{Infrasonidos}: Frecuencias <20 Hz viajan miles de kilómetros con atenuación mínima +\end{itemize} +\end{importante} + +\textbf{Análisis comparativo de atenuación:} + +\begin{formula}[Coeficiente de Atenuación] +\begin{equation} +\alpha \propto \frac{f^2}{\rho c^3} +\end{equation} +Donde $\alpha$ es el coeficiente de atenuación, $f$ la frecuencia, $\rho$ la densidad del medio, y $c$ la velocidad del sonido. Menor atenuación en agua explica el alcance de 100$\times$ mayor para comunicaciones de ballenas. +\end{formula} + +\subsection{Comunicación de Ballenas: Infrasonidos Oceánicos} + +\begin{dato}[Ballena Azul] +Emite infrasonidos profundos (10-40 Hz) que pueden recorrer más de 1600 kilómetros bajo el agua, permitiendo comunicación entre individuos en cuencas oceánicas completas. +\end{dato} + +\textbf{Adaptaciones evolutivas para comunicación de largo alcance:} + +\begin{itemize} + \item \textbf{Frecuencias bajas}: Menor atenuación en agua + \item \textbf{Alta intensidad}: Hasta 188 dB (referenciado a 1 $\mu$Pa a 1 m) + \item \textbf{Repetición}: Patrones repetitivos para aumentar probabilidad de detección + \item \textbf{Canal SOFAR}: Aprovechamiento de canal acústico natural +\end{itemize} + +\section{Eventos Culturales y Fenómenos Sociales} + +\subsection{Festival de Lanzamiento de Vehículos: Glacier View, Alaska} + +\begin{definicion}[Festival de Lanzamiento de Vehículos] +Evento anual en Glacier View, Alaska, donde vehículos son lanzados desde un acantilado de 90 metros hacia un estanque como forma de entretenimiento comunitario y expresión cultural. +\end{definicion} + +\textbf{Especificaciones del evento:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Parámetro} & \textbf{Valor} \\ \midrule +Ubicación & Glacier View, Alaska ($62^\circ$N $145^\circ$W) \\ +Altura del acantilado & 90 metros (295 pies) \\ +Elevación del estanque & ~400 msnm \\ +Temporada & Verano (junio-agosto) \\ +Asistentes & 200-500 espectadores \\ +Vehículos por evento & 10-30 unidades \\ \bottomrule +\end{tabular} +\end{table} + +\begin{formula}[Tiempo de Caída Libre] +\begin{equation} +t = \sqrt{\frac{2h}{g}} = \sqrt{\frac{2 \times 90}{9.81}} \approx 4.28 \text{ segundos} +\end{equation} +\begin{equation} +v_{impacto} = gt = 9.81 \times 4.28 \approx 42 \text{ m/s} \approx 150 \text{ km/h} +\end{equation} +\end{formula} + +\textbf{Aspectos sociológicos:} + +\begin{itemize} + \item \textbf{Identidad comunitaria}: Evento distintivo que define a la comunidad + \item \textbf{Relación con tecnología}: Celebración irónica de cultura automotriz + \item \textbf{Arte efímero}: Decoración de vehículos como expresión artística temporal + \item \textbf{Economía local}: Atracción turística que genera ingresos para comunidad pequeña +\end{itemize} + +\subsection{Gastronomía Espectáculo: Restaurante con Tirolesa} + +\textbf{Sistema de entrega por tirolesa en Bangkok:} + +\begin{itemize} + \item \textbf{Mecanismo}: Sistema de cables con gravedad asistida + \item \textbf{Seguridad}: Arnés de triple punto de anclaje + \item \textbf{Presentación}: Platos en contenedores especiales anti-volteo + \item \textbf{Experiencia}: Cada orden es performance en sí misma +\end{itemize} + +\begin{ejemplo}[Diferenciación Experiencial] +En mercado restauratero saturado de Bangkok, la tirolesa no es solo método de entrega sino \textit{producto principal}: los clientes pagan tanto por el espectáculo de recibir su comida ``volando'' como por la comida misma. +\end{ejemplo} + +\section{Arte y Expresión Creativa} + +\subsection{Vodan Valsikov: Arquitectura Capilar} + +\begin{dato}[Vodan Valsikov] +Barbero ucraniano viralizado por crear patrones geométricos complejos e ilusiones ópticas mediante corte de cabello, transformando la cabeza en lienzo artístico. +\end{dato} + +\textbf{Técnicas empleadas:} + +\begin{itemize} + \item \textbf{Geometría euclidiana}: Patrones basados en figuras regulares + \item \textbf{Perspectiva}: Uso de profundidad para crear ilusiones 3D + \item \textbf{Gradiente}: Variaciones de longitud para efecto de sombreado + \item \textbf{Simetría}: Patrones bilateral y radialmente simétricos +\end{itemize} + +\subsection{Ghost Pittur: Arte Inverso Urbano} + +\begin{definicion}[Arte Inverso / Reverse Graffiti] +Práctica artística que consiste en crear limpiando superficies sucias en lugar de aplicar pigmento; el ``arte'' emerge por sustracción de suciedad en lugar de por adición de material. +\end{definicion} + +\textbf{Metodología de Ghost Pittur:} + +\begin{enumerate} + \item \textbf{Reconocimiento}: Identificación de superficies vandalizadas + \item \textbf{Planificación}: Diseño del patrón de limpieza + \item \textbf{Ejecución}: Limpieza selectiva (generalmente con lavadora a presión) + \item \textbf{Revelación}: El patrón emerge por contraste entre superficie limpia y sucia + \item \textbf{Temporalidad}: Obra efímera que eventualmente será vandalizada nuevamente +\end{enumerate} + +\textbf{Aspectos legales y filosóficos:} + +\begin{itemize} + \item \textbf{Ambigüedad legal}: ¿Es vandalismo revertir vandalismo? + \item \textbf{Consentimiento}: Generalmente realizado sin permiso del propietario + \item \textbf{Valor estético}: ¿Es arte si no hay adición sino sustracción? + \item \textbf{Impermanencia}: Aceptación de la naturaleza temporal del trabajo +\end{itemize} + +\subsection{Wang Liang: El Hombre Invisible} + +\begin{dato}[Camuflaje Humano] +Artista chino especializado en pintura corporal que le permite fundirse completamente con entornos naturales y urbanos, documentando el performance mediante fotografía. +\end{dato} + +\textbf{Proceso de creación:} + +\begin{enumerate} + \item \textbf{Selección del entorno}: Identificación de fondo con potencial para camuflaje + \item \textbf{Documentación del fondo}: Fotografía de alta resolución del entorno + \item \textbf{Preparación de modelo}: Aplicación de pintura corporal base + \item \textbf{Pintura detallada}: Reproducción meticulosa de patrones del entorno + \item \textbf{Posicionamiento}: Alineación precisa del modelo con el fondo + \item \textbf{Documentación final}: Fotografía del performance terminado +\end{enumerate} + +\textbf{Temas explorados:} + +\begin{itemize} + \item \textbf{Identidad}: Disolución del yo en el entorno + \item \textbf{Observación}: Límites entre percepción y realidad + \item \textbf{Naturaleza vs. Cultura}: Integración de humano con entorno natural + \item \textbf{Anonimato}: Desaparición de individualidad en espacios masivos +\end{itemize} + +\section{Cultura Digital y Fenómenos de Redes Sociales} + +\subsection{Récord de YouTube: Avril Lavigne} + +\begin{dato}[Primer Video en 100 Millones] +\textit{"Girlfriend"} de Avril Lavigne fue el primer video en YouTube en alcanzar 100 millones de reproducciones, marcando un hito en la era del contenido viral y estableciendo nuevas métricas de éxito en música digital. +\end{dato} + +\textbf{Contexto histórico:} + +\begin{itemize} + \item \textbf{Año}: 2007 (época temprana de YouTube) + \item \textbf{Plataforma}: YouTube fundado en 2005, en fase de expansión + \item \textbf{Industria musical}: Transición de MTV a YouTube como plataforma principal + \item \textbf{Métricas}: Establecimiento de vistas como medida de éxito +\end{itemize} + +\subsection{Nombres Tecnológicos: ChatGPT Bastidas Guerra} + +\begin{importante}[Caso ChatGPT] +Bebé registrada oficialmente en Cereté, Córdoba, Colombia, con el nombre ``ChatGPT Bastidas Guerra'', reflejando la penetración cultural de sistemas de inteligencia artificial en prácticas de nominación (naming practices). +\end{importante} + +\textbf{Implicaciones sociológicas:} + +\begin{enumerate} + \item \textbf{Digitalización de identidad}: Sistema de IA integrado a documento legal de identidad + \item \textbf{Memoria cultural}: Nombre preserva momento específico de adopción tecnológica + \item \textbf{Identidad anticipada}: Niña cargará marca comercial específica como nombre personal + \item \textbf{Potencial estigma}: Riesgo de asociación con tecnología que puede volverse obsoleta o controversial +\end{enumerate} + +\textbf{Contexto legal:} + +\begin{itemize} + \item \textbf{Ley colombiana}: Nombres deben respetar dignidad del niño + \item \textbf{Interpretación}: ¿Es ``ChatGPT'' un nombre válido bajo estándares de dignidad? + \item \textbf{Precedente}: Casos anteriores de nombres no tradicionales en Colombia +\end{itemize} + +\subsection{Memoria Animal: Ariel la Ninfa} + +\begin{definicion}[Memoria Auditiva Animal] +Capacidad cognitiva de procesar, almacenar y reproducir patrones sonoros complejos, documentada en diversas especies aviares y mamíferas. +\end{definicion} + +\textbf{Caso Ariel -- Ninfa memoriza tono de iPhone:} + +\begin{itemize} + \item \textbf{Especie}: Probablemente \textit{Agapornis} (lovebird) o similar + \item \textbf{Habilidad}: Memorización y reproducción de melodía específica + \item \textbf{Mecanismo}: Aprendizaje vocal por imitación + \item \textbf{Significado}: Demostración de memoria auditiva episódica en aves +\end{itemize} + +\section{Nomenclatura y Coincidencias} + +\subsection{HTX: Dualidad de Denominación} + +\textbf{Coincidencia onomástica:} + +\begin{itemize} + \item \textbf{HTX Studio}: Equipo de ingeniería con sede en China, desarrollador de cubos de basura inteligentes + \item \textbf{Comunidad HTX}: Referencia a comunidad de Glacier View, Alaska + \item \textbf{Similitud}: Ambas entidades utilizan sigla ``HTX'' + \item \textbf{Diferencia}: No existe relación conocida entre las entidades +\end{itemize} + +Este caso ilustra fenómeno de \textbf{convergencia onomástica} --diferentes entidades desarrollando independientemente nombres idénticos o similares--, relativamente común en contexto de globalización y abbreviated naming conventions. + +\section{Análisis Demográfico} + +\subsection{Distribución de Fechas de Nacimiento} + +\begin{dato}[Patrones Estacionales] +Existen variaciones estadísticamente significativas en la frecuencia de nacimientos a lo largo del año, con ciertas fechas siendo considerablemente más comunes o raras que otras. +\end{dato} + +\textbf{Factores que influyen en distribución:} + +\begin{table}[h] +\centering +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Factor} & \textbf{Efecto} \\ \midrule +Concepción estacional & Picos en nacimientos 9 meses después \\ +Condiciones climáticas & Menos concepciones en temperaturas extremas \\ +Planificación cultural & Evitación de fechas festivas \\ +Intervención médica & Cesáreas programadas en horarios laborales \\ +Festividades & Aumento de concepciones durante vacaciones \\ \bottomrule +\end{tabular} +\end{table} + +\textbf{Fechas estadísticamente extremas (EE.UU.):} + +\begin{itemize} + \item \textbf{Más común}: Septiembre 16 (concepción navideña) + \item \textbf{Más rara}: Diciembre 25 (evitación de partos en Navidad) + \item \textbf{Segunda más común}: Septiembre 9 + \item \textbf{Segunda más rara}: Enero 1 (evitación de Año Nuevo) +\end{itemize} + +\begin{center} +\begin{tikzpicture} + \begin{axis}[ + width=12cm, + height=4cm, + xlabel={Mes}, + ylabel={Nacimientos (relativo a media)}, + xtick={1,2,3,4,5,6,7,8,9,10,11,12}, + xticklabels={Ene,Feb,Mar,Abr,May,Jun,Jul,Ago,Sep,Oct,Nov,Dic}, + ymin=0.8, ymax=1.2, + grid=major, + ] + \addplot[smooth, blue, thick] coordinates { + (1, 0.95) (2, 0.92) (3, 0.98) (4, 0.99) (5, 1.01) (6, 1.02) + (7, 1.03) (8, 1.05) (9, 1.12) (10, 1.08) (11, 1.02) (12, 0.85) + }; + \end{axis} +\end{tikzpicture} +\end{center} + +\section{Síntesis y Conclusiones} + +\subsection{Temas Transversales Identificados} + +La compilación de estas veinticinco curiosidades revela varios hilos conductores que conectan fenómenos aparentemente dispares: + +\textbf{1. Adaptación y Evolución} + +\begin{itemize} + \item Adaptación biológica: camuflaje del pez murciélago + \item Adaptación cultural: hot dog francés como variación local de producto global + \item Adaptación tecnológica: sistemas que aprenden y responden (cubos de basura inteligentes) +\end{itemize} + +\textbf{2. Percepción y Realidad} + +\begin{itemize} + \item Psicología: efecto señuelo manipula percepción de valor + \item Arte: Wang Liang y Ghost Pittur juegan con límites de percepción visual + \item Tecnología: realidad aumentada modifica percepción del entorno +\end{itemize} + +\textbf{3. Optimización de Recursos} + +\begin{itemize} + \item Biológica: pez murciélago ahorra energía mediante camuflaje pasivo + \item Industrial: Lego optimiza tolerancias para máxima compatibilidad + \item Económica: empresas diseñan arquitecturas de choice para maximizar ganancias +\end{itemize} + +\textbf{4. Expresión y Creatividad} + +\begin{itemize} + \item Gastronómica: innovaciones como hot dog francés o ketchup smoothie + \item Artística: Vodan Valsikov, Ghost Pittur, Wang Liang + \item Tecnológica: diseño de productos que incorporan creatividad (carritos incolisionables) +\end{itemize} + +\subsection{Valor del Conocimiento Interconectado} + +Este ejercicio de compilación demuestra que: + +\begin{enumerate} + \item \textbf{Curiosidad como motor de aprendizaje}: Puntos de entrada triviales pueden llevar a comprensión profunda de conceptos complejos + \item \textbf{Interdisciplinariedad}: Fenómenos en un área (biología) pueden iluminar understanding en otra (ingeniería) + \item \textbf{Contextualización}: Datos aislados adquieren significado cuando conectados con patrones más amplios + \item \textbf{Serendipia}: Encuentros fortuitos entre conceptos aparentemente no relacionados generan nuevas insights +\end{enumerate} + +\begin{importante}[Conclusión Principal] +Las ``cosas que no sabías hace cinco minutos'' --aunque aparentemente triviales-- funcionan como \textit{portales de entrada} a understanding más profundo de principios científicos, fenómenos culturales y patrones de comportamiento. Cada curiosidad es nodo en red de conocimiento, con conexiones que se extienden en múltiples direcciones hacia áreas tradicionalmente separadas del saber humano. +\end{importante} + +\section*{Glosario} + +\begin{description}[style=multiline, leftmargin=3.5cm, font=\bfseries] + +\item[ABS] \textit{Acrylonitrile Butadiene Styrene}. Termoplástico utilizado en piezas Lego por su resistencia, durabilidad y precisión de moldeo. + +\item[Atenuación sonora] Pérdida de intensidad de una onda sonora a medida que se propaga a través de un medio, dependiente de frecuencia y propiedades del medio. + +\item[Açaí] Fruta de la palmera \textit{Euterpe oleracea}, nativa de región amazónica, rica en antioxidantes y antocianinas. + +\item[Biofilm] Comunidad microbiana adherida a superficie, encapsulada en matriz extracelular polimérica; en contexto dental, ``placa bacteriana''. + +\item[Canal SOFAR] \textit{Sound Fixing and Ranging Channel}. Capa oceánica donde la velocidad del sonido alcanza mínimo, creando guía de ondas naturales para comunicación de larga distancia. + +\item[Camuflaje] Adaptación que permite organismo mezclarse visualmente con su entorno, evitando detección por depredadores o presas. + +\item[Choice architecture] Diseño del entorno en que las personas toman decisiones, para influir en elección sin restringir opciones (concepto de Thaler y Sunstein). + +\item[Decoy effect] Ver ``Efecto señuelo''. + +\item[Definición operacional] Definición de concepto en términos de procedimientos o mediciones específicas, permitiendo su replicación experimental. + +\item[Dióxido de titanio] Pigmento blanco ($TiO_2$) utilizado en plásticos; actúa como filler reforzante aumentando resistencia mecánica. + +\item[Efecto señuelo] Sesgo cognitivo donde una opción poco atractiva (señuelo) modifica preferencias entre dos opciones principales. + +\item[Emmental] Queso suizo de origen, caracterizado por agujeres formados por bubbles de $CO_2$ durante fermentación. + +\item[Encapsulamiento] En programación orientada a objetos, ocultación de detalles de implementación exponiendo solo interfaz pública. + +\item[Episodio memorial] Memoria de evento específico contextualizado en tiempo y espacio, con detalle fenomenológico. + +\item[Frame] En teoría de decisiones, marco conceptual que influencia cómo opciones son percibidas y evaluadas. + +\item[Glocalización] Estrategia de adaptar productos globales a preferencias locales, manteniendo elementos de identidad global. + +\item[Gruyère] Queso suizo similar a emmental, utilizado en cocina francesa por sus características de fusión. + +\item[Heuristic] Atajo mental simplificador que reduce carga cognitiva en toma de decisiones. + +\item[HTX Studio] Equipo de ingeniería y tecnología con base en China, desarrollador de sistemas de cubos de basura inteligentes. + +\item[Infrasonido] Sonido de frecuencia inferior a 20 Hz, por debajo del umbral auditivo humano pero detectable por algunos animales. + +\item[Mechanics] Rama de física que estudia movimiento y fuerzas; en ingeniería, aplicación de principios mecánicos a diseño de sistemas. + +\item[Mimetismo] Capacidad de organismo para imitar apariencia de otro objeto u organismo, obteniendo ventaja evolutiva. + +\item[Machine Learning] Subcampo de inteligencia artificial enfocado en desarrollo de algoritmos que aprenden patrones a partir de datos. + +\item[Neural network] Arquitectura computacional inspirada en redes neuronales biológicas, utilizada para reconocimiento de patrones. + +\item[Ontogenia] Desarrollo de organismo individual desde fertilización hasta madurez. + +\item[Placa dental] Biofilm adherido a superficie dental, compuesto por bacterias (principalmente \textit{Streptococcus mutans}), restos alimenticios y polímeros bacterianos. + +\item[Repulsión magnética] Fuerza entre polos magnéticos iguales que causa alejamiento mutuo, descrita por ley de Coulomb magnética. + +\item[Reverse graffiti] Ver ``Arte inverso''. + +\item[Serendipia] Hallazgo fortuito o afortunado de algo valioso no buscado originalmente. + +\item[Streptococcus mutans] Bacteria grampositiva, anaerobia facultativa, principal agente etiológico de caries dental en humanos. + +\item[Sulforafano] Compuesto organosulfurado presente en vegetales crucíferos (brócoli, coliflor), con propiedades antibacterianas y antioxidantes. + +\item[Tirolesa] Sistema de transporte consistente en cable tendido entre dos puntos, por el cual se desplaza persona o vehículo suspendido. + +\item[Xpeng] Fabricante chino de vehículos eléctricos, conocido por integrar tecnología avanzada de realidad aumentada en automóviles. + +\end{description} + +\section*{Referencias} + +\noindent La información presentada en este documento ha sido compilada de diversas fuentes incluyendo documentación científica, reportajes de medios, y observaciones de fenómenos culturales contemporáneos. Los datos específicos sobre resistencia de piezas Lego, características del efecto señuelo, y propiedades del brócoli están respaldados por literatura científica y técnica en las respectivas áreas. + +\noindent Para mayor profundización en los temas presentados, se recomienda consultar: + +\begin{itemize} + \item Literatura especializada en ciencia de materiales para análisis de pigmentos en polímeros + \item Investigaciones en economía conductual para estudio del efecto señuelo + \item Textos de bioacústica para comunicación de cetáceos + \item Documentación etológica para mimetismo en peces y otros organismos + \item Fuentes de sociología y antropología para análisis de fenómenos culturales contemporáneos +\end{itemize} + +\end{document} diff --git a/services/ai/base_provider.py b/services/ai/base_provider.py index 9c9087c..f1d8a97 100644 --- a/services/ai/base_provider.py +++ b/services/ai/base_provider.py @@ -28,6 +28,11 @@ class AIProvider(ABC): """Generate text from prompt""" pass + @abstractmethod + def fix_latex(self, latex_code: str, error_log: str, **kwargs) -> str: + """Fix broken LaTeX code based on compiler error log""" + pass + @abstractmethod def is_available(self) -> bool: """Check if provider is available and configured""" diff --git a/services/ai/claude_provider.py b/services/ai/claude_provider.py index cf4df1e..1e6b5fb 100644 --- a/services/ai/claude_provider.py +++ b/services/ai/claude_provider.py @@ -52,13 +52,14 @@ class ClaudeProvider(AIProvider): return env - def _run_cli(self, prompt: str, timeout: int = 300) -> str: - """Run Claude CLI with prompt""" + def _run_cli(self, prompt: str, timeout: int = 600) -> str: + """Run Claude CLI with prompt using -p flag for stdin input""" if not self.is_available(): raise AIProcessingError("Claude CLI not available or not configured") try: - cmd = [self._cli_path] + # Use -p flag to read prompt from stdin, --dangerously-skip-permissions for automation + cmd = [self._cli_path, "--dangerously-skip-permissions", "-p", "-"] process = subprocess.run( cmd, input=prompt, @@ -126,3 +127,32 @@ Return only the category name, nothing else.""" def generate_text(self, prompt: str, **kwargs) -> str: """Generate text using Claude""" return self._run_cli(prompt) + + def fix_latex(self, latex_code: str, error_log: str, **kwargs) -> str: + """Fix broken LaTeX code using Claude""" + prompt = f"""I have a LaTeX file that failed to compile. Please fix the code. + +COMPILER ERROR LOG: +{error_log[-3000:]} + +BROKEN LATEX CODE: +{latex_code} + +INSTRUCTIONS: +1. Analyze the error log to find the specific syntax error. +2. Fix the LaTeX code. +3. Return ONLY the full corrected LaTeX code. +4. Do not include markdown blocks or explanations. +5. Start immediately with \\documentclass. + +COMMON LATEX ERRORS TO CHECK: +- TikZ nodes with line breaks (\\\\) MUST have "align=center" in their style. + WRONG: \\node[box] (n) {{Text\\\\More}}; + CORRECT: \\node[box, align=center] (n) {{Text\\\\More}}; +- All \\begin{{env}} must have matching \\end{{env}} +- All braces {{ }} must be balanced +- Math mode $ must be paired +- Special characters need escaping: % & # _ +- tcolorbox environments need proper titles: [Title] not {{Title}} +""" + return self._run_cli(prompt, timeout=180) diff --git a/services/ai/gemini_provider.py b/services/ai/gemini_provider.py index af580b1..50d907c 100644 --- a/services/ai/gemini_provider.py +++ b/services/ai/gemini_provider.py @@ -1,6 +1,7 @@ """ Gemini AI Provider - Optimized version with rate limiting and retry """ + import logging import subprocess import shutil @@ -16,31 +17,32 @@ from .base_provider import AIProvider class TokenBucket: """Token bucket rate limiter""" - + def __init__(self, rate: float = 10, capacity: int = 20): self.rate = rate # tokens per second self.capacity = capacity self.tokens = capacity self.last_update = time.time() self._lock = None # Lazy initialization - + def _get_lock(self): if self._lock is None: import threading + self._lock = threading.Lock() return self._lock - + def acquire(self, tokens: int = 1) -> float: with self._get_lock(): now = time.time() elapsed = now - self.last_update self.last_update = now self.tokens = min(self.capacity, self.tokens + elapsed * self.rate) - + if self.tokens >= tokens: self.tokens -= tokens return 0.0 - + wait_time = (tokens - self.tokens) / self.rate self.tokens = 0 return wait_time @@ -48,7 +50,7 @@ class TokenBucket: class CircuitBreaker: """Circuit breaker for API calls""" - + def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60): self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout @@ -56,21 +58,26 @@ class CircuitBreaker: self.last_failure: Optional[datetime] = None self.state = "closed" # closed, open, half-open self._lock = None - + def _get_lock(self): if self._lock is None: import threading + self._lock = threading.Lock() return self._lock - + def call(self, func, *args, **kwargs): with self._get_lock(): if self.state == "open": - if self.last_failure and (datetime.utcnow() - self.last_failure).total_seconds() > self.recovery_timeout: + if ( + self.last_failure + and (datetime.utcnow() - self.last_failure).total_seconds() + > self.recovery_timeout + ): self.state = "half-open" else: raise AIProcessingError("Circuit breaker is open") - + try: result = func(*args, **kwargs) if self.state == "half-open": @@ -87,7 +94,7 @@ class CircuitBreaker: class GeminiProvider(AIProvider): """Gemini AI provider with rate limiting and retry""" - + def __init__(self): super().__init__() self.logger = logging.getLogger(__name__) @@ -102,17 +109,17 @@ class GeminiProvider(AIProvider): "max_attempts": 3, "base_delay": 1.0, "max_delay": 30.0, - "exponential_base": 2 + "exponential_base": 2, } - + @property def name(self) -> str: return "Gemini" - + def is_available(self) -> bool: """Check if Gemini CLI or API is available""" return bool(self._cli_path or self._api_key) - + def _init_session(self) -> None: """Initialize HTTP session with connection pooling""" if self._session is None: @@ -120,17 +127,17 @@ class GeminiProvider(AIProvider): adapter = requests.adapters.HTTPAdapter( pool_connections=10, pool_maxsize=20, - max_retries=0 # We handle retries manually + max_retries=0, # We handle retries manually ) - self._session.mount('https://', adapter) - + self._session.mount("https://", adapter) + def _run_with_retry(self, func, *args, **kwargs): """Execute function with exponential backoff retry""" max_attempts = self._retry_config["max_attempts"] base_delay = self._retry_config["base_delay"] - + last_exception = None - + for attempt in range(max_attempts): try: return self._circuit_breaker.call(func, *args, **kwargs) @@ -138,94 +145,84 @@ class GeminiProvider(AIProvider): last_exception = e if attempt < max_attempts - 1: delay = min( - base_delay * (2 ** attempt), - self._retry_config["max_delay"] + base_delay * (2**attempt), self._retry_config["max_delay"] ) # Add jitter delay += delay * 0.1 * (time.time() % 1) - self.logger.warning(f"Attempt {attempt + 1} failed: {e}, retrying in {delay:.2f}s") + self.logger.warning( + f"Attempt {attempt + 1} failed: {e}, retrying in {delay:.2f}s" + ) time.sleep(delay) - + raise AIProcessingError(f"Max retries exceeded: {last_exception}") - + def _run_cli(self, prompt: str, use_flash: bool = True, timeout: int = 300) -> str: """Run Gemini CLI with prompt""" if not self._cli_path: raise AIProcessingError("Gemini CLI not available") - + model = self._flash_model if use_flash else self._pro_model cmd = [self._cli_path, model, prompt] - + try: # Apply rate limiting wait_time = self._rate_limiter.acquire() if wait_time > 0: time.sleep(wait_time) - + process = subprocess.run( - cmd, - text=True, - capture_output=True, - timeout=timeout, - shell=False + cmd, text=True, capture_output=True, timeout=timeout, shell=False ) - + if process.returncode != 0: error_msg = process.stderr or "Unknown error" raise AIProcessingError(f"Gemini CLI failed: {error_msg}") - + return process.stdout.strip() except subprocess.TimeoutExpired: raise AIProcessingError(f"Gemini CLI timed out after {timeout}s") except Exception as e: raise AIProcessingError(f"Gemini CLI error: {e}") - + def _call_api(self, prompt: str, use_flash: bool = True, timeout: int = 180) -> str: """Call Gemini API with rate limiting and retry""" if not self._api_key: raise AIProcessingError("Gemini API key not configured") - + self._init_session() - + model = self._flash_model if use_flash else self._pro_model url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent" - - payload = { - "contents": [{ - "parts": [{"text": prompt}] - }] - } - + + payload = {"contents": [{"parts": [{"text": prompt}]}]} + params = {"key": self._api_key} - + def api_call(): # Apply rate limiting wait_time = self._rate_limiter.acquire() if wait_time > 0: time.sleep(wait_time) - + response = self._session.post( - url, - json=payload, - params=params, - timeout=timeout + url, json=payload, params=params, timeout=timeout ) response.raise_for_status() return response - + response = self._run_with_retry(api_call) data = response.json() - + if "candidates" not in data or not data["candidates"]: raise AIProcessingError("Empty response from Gemini API") - + candidate = data["candidates"][0] if "content" not in candidate or "parts" not in candidate["content"]: raise AIProcessingError("Invalid response format from Gemini API") - + result = candidate["content"]["parts"][0]["text"] return result.strip() - + def _run(self, prompt: str, use_flash: bool = True, timeout: int = 300) -> str: """Run Gemini with fallback between CLI and API""" # Try CLI first if available @@ -234,14 +231,14 @@ class GeminiProvider(AIProvider): return self._run_cli(prompt, use_flash, timeout) except Exception as e: self.logger.warning(f"Gemini CLI failed, trying API: {e}") - + # Fallback to API if self._api_key: api_timeout = min(timeout, 180) return self._call_api(prompt, use_flash, api_timeout) - + raise AIProcessingError("No Gemini provider available (CLI or API)") - + def summarize(self, text: str, **kwargs) -> str: """Generate summary using Gemini""" prompt = f"""Summarize the following text: @@ -250,7 +247,7 @@ class GeminiProvider(AIProvider): Provide a clear, concise summary in Spanish.""" return self._run(prompt, use_flash=True) - + def correct_text(self, text: str, **kwargs) -> str: """Correct text using Gemini""" prompt = f"""Correct the following text for grammar, spelling, and clarity: @@ -259,11 +256,16 @@ Provide a clear, concise summary in Spanish.""" Return only the corrected text, nothing else.""" return self._run(prompt, use_flash=True) - + def classify_content(self, text: str, **kwargs) -> Dict[str, Any]: """Classify content using Gemini""" - categories = ["historia", "analisis_contable", "instituciones_gobierno", "otras_clases"] - + categories = [ + "historia", + "analisis_contable", + "instituciones_gobierno", + "otras_clases", + ] + prompt = f"""Classify the following text into one of these categories: - historia - analisis_contable @@ -274,39 +276,61 @@ Text: {text} Return only the category name, nothing else.""" result = self._run(prompt, use_flash=True).lower() - + # Validate result if result not in categories: result = "otras_clases" - - return { - "category": result, - "confidence": 0.9, - "provider": self.name - } + + return {"category": result, "confidence": 0.9, "provider": self.name} def generate_text(self, prompt: str, **kwargs) -> str: """Generate text using Gemini""" - use_flash = kwargs.get('use_flash', True) + use_flash = kwargs.get("use_flash", True) if self._api_key: return self._call_api(prompt, use_flash=use_flash) - return self._call_cli(prompt, use_yolo=True) - + return self._run_cli(prompt, use_flash=use_flash) + + def fix_latex(self, latex_code: str, error_log: str, **kwargs) -> str: + """Fix broken LaTeX code using Gemini""" + prompt = f"""Fix the following LaTeX code which failed to compile. + +Error Log: +{error_log[-3000:]} + +Broken Code: +{latex_code} + +INSTRUCTIONS: +1. Return ONLY the corrected LaTeX code. No explanations. +2. Start immediately with \\documentclass. + +COMMON LATEX ERRORS TO FIX: +- TikZ nodes with line breaks (\\\\) MUST have "align=center" in their style. + WRONG: \\node[box] (n) {{Text\\\\More}}; + CORRECT: \\node[box, align=center] (n) {{Text\\\\More}}; +- All \\begin{{env}} must have matching \\end{{env}} +- All braces {{ }} must be balanced +- Math mode $ must be paired +- Special characters need escaping: % & # _ +- tcolorbox environments need proper titles: [Title] not {{Title}} +""" + return self._run(prompt, use_flash=False) # Use Pro model for coding fixes + def get_stats(self) -> Dict[str, Any]: """Get provider statistics""" return { "rate_limiter": { "tokens": round(self._rate_limiter.tokens, 2), "capacity": self._rate_limiter.capacity, - "rate": self._rate_limiter.rate + "rate": self._rate_limiter.rate, }, "circuit_breaker": { "state": self._circuit_breaker.state, "failures": self._circuit_breaker.failures, - "failure_threshold": self._circuit_breaker.failure_threshold + "failure_threshold": self._circuit_breaker.failure_threshold, }, "cli_available": bool(self._cli_path), - "api_available": bool(self._api_key) + "api_available": bool(self._api_key), } diff --git a/services/ai/parallel_provider.py b/services/ai/parallel_provider.py new file mode 100644 index 0000000..b5ba549 --- /dev/null +++ b/services/ai/parallel_provider.py @@ -0,0 +1,346 @@ +""" +Parallel AI Provider - Race multiple providers for fastest response +Implements Strategy A: Parallel Generation with Consensus +""" + +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from typing import Dict, List, Optional, Any +from datetime import datetime + +from core import AIProcessingError +from .base_provider import AIProvider + + +@dataclass +class ProviderResult: + """Result from a single provider""" + provider_name: str + content: str + duration_ms: int + success: bool + error: Optional[str] = None + quality_score: float = 0.0 + + +@dataclass +class ParallelResult: + """Aggregated result from parallel execution""" + content: str + strategy: str + providers_used: List[str] + total_duration_ms: int + all_results: List[ProviderResult] + selected_provider: str + + +class ParallelAIProvider: + """ + Orchestrates multiple AI providers in parallel for faster responses. + + Strategies: + - "race": Use first successful response (fastest) + - "consensus": Wait for all, select best quality + - "majority": Select most common response + """ + + def __init__(self, providers: Dict[str, AIProvider], max_workers: int = 4): + self.providers = providers + self.max_workers = max_workers + self.logger = logging.getLogger(__name__) + self.executor = ThreadPoolExecutor(max_workers=max_workers) + + def _generate_sync(self, provider: AIProvider, prompt: str, **kwargs) -> ProviderResult: + """Synchronous wrapper for provider generation""" + start_time = datetime.now() + try: + content = provider.generate_text(prompt, **kwargs) + duration_ms = int((datetime.now() - start_time).total_seconds() * 1000) + + # Calculate quality score + quality_score = self._calculate_quality_score(content) + + return ProviderResult( + provider_name=provider.name, + content=content, + duration_ms=duration_ms, + success=True, + quality_score=quality_score + ) + except Exception as e: + duration_ms = int((datetime.now() - start_time).total_seconds() * 1000) + self.logger.error(f"{provider.name} failed: {e}") + return ProviderResult( + provider_name=provider.name, + content="", + duration_ms=duration_ms, + success=False, + error=str(e) + ) + + def _calculate_quality_score(self, content: str) -> float: + """Calculate quality score for generated content""" + score = 0.0 + + # Length check (comprehensive is better) + if 500 < len(content) < 50000: + score += 0.2 + + # LaTeX structure validation + latex_indicators = [ + r"\documentclass", + r"\begin{document}", + r"\section", + r"\subsection", + r"\begin{itemize}", + r"\end{document}" + ] + found_indicators = sum(1 for ind in latex_indicators if ind in content) + score += (found_indicators / len(latex_indicators)) * 0.4 + + # Bracket matching + if content.count("{") == content.count("}"): + score += 0.2 + + # Environment closure + envs = ["document", "itemize", "enumerate"] + for env in envs: + if content.count(f"\\begin{{{env}}}") == content.count(f"\\end{{{env}}}"): + score += 0.1 + + # Has content beyond template + if len(content) > 1000: + score += 0.1 + + return min(score, 1.0) + + def generate_parallel( + self, + prompt: str, + strategy: str = "race", + timeout_ms: int = 300000, # 5 minutes default + **kwargs + ) -> ParallelResult: + """ + Execute prompt across multiple providers in parallel. + + Args: + prompt: The prompt to send to all providers + strategy: "race", "consensus", or "majority" + timeout_ms: Maximum time to wait for results + **kwargs: Additional arguments for providers + + Returns: + ParallelResult with selected content and metadata + """ + if not self.providers: + raise AIProcessingError("No providers available for parallel execution") + + start_time = datetime.now() + all_results: List[ProviderResult] = [] + + # Submit all providers + futures = {} + for name, provider in self.providers.items(): + if provider.is_available(): + future = self.executor.submit( + self._generate_sync, + provider, + prompt, + **kwargs + ) + futures[future] = name + + # Wait for results based on strategy + if strategy == "race": + all_results = self._race_strategy(futures, timeout_ms) + elif strategy == "consensus": + all_results = self._consensus_strategy(futures, timeout_ms) + elif strategy == "majority": + all_results = self._majority_strategy(futures, timeout_ms) + else: + raise ValueError(f"Unknown strategy: {strategy}") + + # Select best result + selected = self._select_result(all_results, strategy) + + total_duration_ms = int((datetime.now() - start_time).total_seconds() * 1000) + + self.logger.info( + f"Parallel generation complete: {strategy} strategy, " + f"{len(all_results)} providers, {selected.provider_name} selected, " + f"{total_duration_ms}ms" + ) + + return ParallelResult( + content=selected.content, + strategy=strategy, + providers_used=[r.provider_name for r in all_results if r.success], + total_duration_ms=total_duration_ms, + all_results=all_results, + selected_provider=selected.provider_name + ) + + def _race_strategy( + self, + futures: dict, + timeout_ms: int + ) -> List[ProviderResult]: + """Return first successful response""" + results = [] + for future in as_completed(futures, timeout=timeout_ms / 1000): + try: + result = future.result() + results.append(result) + if result.success: + # Got a successful response, cancel remaining + for f in futures: + f.cancel() + break + except Exception as e: + self.logger.error(f"Future failed: {e}") + return results + + def _consensus_strategy( + self, + futures: dict, + timeout_ms: int + ) -> List[ProviderResult]: + """Wait for all, return all results""" + results = [] + for future in as_completed(futures, timeout=timeout_ms / 1000): + try: + result = future.result() + results.append(result) + except Exception as e: + self.logger.error(f"Future failed: {e}") + return results + + def _majority_strategy( + self, + futures: dict, + timeout_ms: int + ) -> List[ProviderResult]: + """Wait for majority, select most common response""" + results = [] + for future in as_completed(futures, timeout=timeout_ms / 1000): + try: + result = future.result() + results.append(result) + except Exception as e: + self.logger.error(f"Future failed: {e}") + return results + + def _select_result(self, results: List[ProviderResult], strategy: str) -> ProviderResult: + """Select best result based on strategy""" + successful = [r for r in results if r.success] + + if not successful: + # Return first failed result with error info + return results[0] if results else ProviderResult( + provider_name="none", + content="", + duration_ms=0, + success=False, + error="All providers failed" + ) + + if strategy == "race" or len(successful) == 1: + return successful[0] + + if strategy == "consensus": + # Select by quality score + return max(successful, key=lambda r: r.quality_score) + + if strategy == "majority": + # Group by similar content (simplified - use longest) + return max(successful, key=lambda r: len(r.content)) + + return successful[0] + + def fix_latex_parallel( + self, + latex_code: str, + error_log: str, + timeout_ms: int = 120000, + **kwargs + ) -> ParallelResult: + """Try to fix LaTeX across multiple providers in parallel""" + # Build fix prompt for each provider + results = [] + start_time = datetime.now() + + for name, provider in self.providers.items(): + if provider.is_available(): + try: + start = datetime.now() + fixed = provider.fix_latex(latex_code, error_log, **kwargs) + duration_ms = int((datetime.now() - start).total_seconds() * 1000) + + # Score by checking if error patterns are reduced + quality = self._score_latex_fix(fixed, error_log) + + results.append(ProviderResult( + provider_name=name, + content=fixed, + duration_ms=duration_ms, + success=True, + quality_score=quality + )) + except Exception as e: + self.logger.error(f"{name} fix failed: {e}") + + # Select best fix + if results: + selected = max(results, key=lambda r: r.quality_score) + total_duration_ms = int((datetime.now() - start_time).total_seconds() * 1000) + + return ParallelResult( + content=selected.content, + strategy="consensus", + providers_used=[r.provider_name for r in results], + total_duration_ms=total_duration_ms, + all_results=results, + selected_provider=selected.provider_name + ) + + raise AIProcessingError("All providers failed to fix LaTeX") + + def _score_latex_fix(self, fixed_latex: str, original_error: str) -> float: + """Score a LaTeX fix attempt""" + score = 0.5 # Base score + + # Check if common error patterns are addressed + error_patterns = [ + ("Undefined control sequence", r"\\[a-zA-Z]+"), + ("Missing $ inserted", r"\$.*\$"), + ("Runaway argument", r"\{.*\}"), + ] + + for error_msg, pattern in error_patterns: + if error_msg in original_error: + # If error was in original, check if pattern appears better + score += 0.1 + + # Validate bracket matching + if fixed_latex.count("{") == fixed_latex.count("}"): + score += 0.2 + + # Validate environment closure + envs = ["document", "itemize", "enumerate"] + for env in envs: + begin_count = fixed_latex.count(f"\\begin{{{env}}}") + end_count = fixed_latex.count(f"\\end{{{env}}}") + if begin_count == end_count: + score += 0.1 + + return min(score, 1.0) + + def shutdown(self): + """Shutdown the executor""" + self.executor.shutdown(wait=True) + + def __del__(self): + self.shutdown() diff --git a/services/ai/prompt_manager.py b/services/ai/prompt_manager.py new file mode 100644 index 0000000..66ebc6f --- /dev/null +++ b/services/ai/prompt_manager.py @@ -0,0 +1,343 @@ +""" +Prompt Manager - Centralized prompt management using resumen.md as source of truth +""" + +import re +import os +from pathlib import Path +from typing import Optional, Dict, Any +from config import settings + + +class PromptManager: + """ + Manages prompts for AI services, loading templates from latex/resumen.md + This is the SINGLE SOURCE OF TRUTH for academic summary generation. + """ + + _instance = None + _prompt_cache: Optional[str] = None + _latex_preamble_cache: Optional[str] = None + + # Path to the prompt template file + PROMPT_FILE_PATH = Path("latex/resumen.md") + + def __new__(cls): + if cls._instance is None: + cls._instance = super(PromptManager, cls).__new__(cls) + return cls._instance + + def _load_prompt_template(self) -> str: + """Load the complete prompt template from resumen.md""" + if self._prompt_cache: + return self._prompt_cache + + try: + file_path = self.PROMPT_FILE_PATH.resolve() + + if not file_path.exists(): + self._prompt_cache = self._get_fallback_prompt() + return self._prompt_cache + + content = file_path.read_text(encoding="utf-8") + + # The file has a markdown code block after "## Prompt Template" + # We need to find the content from "## Prompt Template" to the LAST ``` + # (because there's a ```latex...``` block INSIDE the template) + + # First, find where "## Prompt Template" starts + template_start = content.find("## Prompt Template") + if template_start == -1: + self._prompt_cache = self._get_fallback_prompt() + return self._prompt_cache + + # Find the opening ``` after the header + after_header = content[template_start:] + code_block_start = after_header.find("```") + if code_block_start == -1: + self._prompt_cache = self._get_fallback_prompt() + return self._prompt_cache + + # Skip the opening ``` and any language specifier + after_code_start = after_header[code_block_start + 3:] + first_newline = after_code_start.find("\n") + if first_newline != -1: + actual_content_start = template_start + code_block_start + 3 + first_newline + 1 + else: + actual_content_start = template_start + code_block_start + 3 + + # Now find the LAST ``` that closes the main block + # We look for ``` followed by optional space and then newline or end + remaining = content[actual_content_start:] + + # Find all positions of ``` in the remaining content + positions = [] + pos = 0 + while True: + found = remaining.find("```", pos) + if found == -1: + break + positions.append(found) + pos = found + 3 + + if not positions: + self._prompt_cache = self._get_fallback_prompt() + return self._prompt_cache + + # The LAST ``` is the closing of the main block + # (all previous ``` are the latex block inside the template) + last_backtick_pos = positions[-1] + + # Extract the content + template_content = content[actual_content_start:actual_content_start + last_backtick_pos] + + # Remove leading newline if present + template_content = template_content.lstrip("\n") + + self._prompt_cache = template_content + return self._prompt_cache + + except Exception as e: + print(f"Error loading prompt file: {e}") + self._prompt_cache = self._get_fallback_prompt() + return self._prompt_cache + + def _get_fallback_prompt(self) -> str: + """Fallback prompt if resumen.md is not found""" + return """Sos un asistente académico experto. Creá un resumen extenso en LaTeX basado en la transcripción de clase. + +## Transcripción de clase: +[PEGAR TRANSCRIPCIÓN AQUÍ] + +## Material bibliográfico: +[PEGAR TEXTO DEL LIBRO/APUNTE O INDICAR QUE LO SUBISTE COMO ARCHIVO] + +Generá un archivo LaTeX completo con: +- Estructura académica formal +- Mínimo 10 páginas de contenido +- Fórmulas matemáticas en LaTeX +- Tablas y diagramas cuando corresponda +""" + + def _load_latex_preamble(self) -> str: + """Extract the LaTeX preamble from resumen.md""" + if self._latex_preamble_cache: + return self._latex_preamble_cache + + try: + file_path = self.PROMPT_FILE_PATH.resolve() + + if not file_path.exists(): + return self._get_default_preamble() + + content = file_path.read_text(encoding="utf-8") + + # Extract LaTeX code block in the template + match = re.search( + r"```latex\s*\n([\s\S]*?)\n```", + content + ) + + if match: + self._latex_preamble_cache = match.group(1).strip() + else: + self._latex_preamble_cache = self._get_default_preamble() + + return self._latex_preamble_cache + + except Exception as e: + print(f"Error loading LaTeX preamble: {e}") + return self._get_default_preamble() + + def _get_default_preamble(self) -> str: + """Default LaTeX preamble""" + return r"""\documentclass[11pt,a4paper]{article} +\usepackage[utf8]{inputenc} +\usepackage[spanish,provide=*]{babel} +\usepackage{amsmath,amssymb} +\usepackage{geometry} +\usepackage{graphicx} +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,shapes.geometric,calc} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{fancyhdr} +\usepackage{titlesec} +\usepackage{tcolorbox} +\usepackage{array} +\usepackage{multirow} + +\geometry{margin=2.5cm} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{[MATERIA] - CBC} +\fancyhead[R]{Clase [N]} +\fancyfoot[C]{\thepage} + +% Cajas para destacar contenido +\newtcolorbox{definicion}[1][]{ + colback=blue!5!white, + colframe=blue!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{importante}[1][]{ + colback=red!5!white, + colframe=red!75!black, + fonttitle=\bfseries, + title=#1 +} + +\newtcolorbox{ejemplo}[1][]{ + colback=green!5!white, + colframe=green!50!black, + fonttitle=\bfseries, + title=#1 +} +""" + + def get_latex_summary_prompt( + self, + transcription: str, + materia: str = "Economía", + bibliographic_text: Optional[str] = None, + class_number: Optional[int] = None + ) -> str: + """ + Generate the complete prompt for LaTeX academic summary based on resumen.md template. + + Args: + transcription: The class transcription text + materia: Subject name (default: "Economía") + bibliographic_text: Optional supporting text from books/notes + class_number: Optional class number for header + + Returns: + Complete prompt string ready to send to AI + """ + template = self._load_prompt_template() + + # CRITICAL: Prepend explicit instructions to force direct LaTeX generation + # (This doesn't modify resumen.md, just adds context before it) + explicit_instructions = """CRITICAL: Tu respuesta debe ser ÚNICAMENTE código LaTeX. + +INSTRUCCIONES OBLIGATORIAS: +1. NO incluyas explicaciones previas +2. NO describas lo que vas a hacer +3. Comienza INMEDIATAMENTE con \\documentclass +4. Tu respuesta debe ser SOLO el código LaTeX fuente +5. Termina con \\end{document} + +--- + +""" + + prompt = explicit_instructions + template + + # Replace placeholders + prompt = prompt.replace("[MATERIA]", materia) + + # Insert transcription + if "[PEGAR TRANSCRIPCIÓN AQUÍ]" in prompt: + prompt = prompt.replace("[PEGAR TRANSCRIPCIÓN AQUÍ]", transcription) + else: + prompt += f"\n\n## Transcripción de clase:\n{transcription}" + + # Insert bibliographic material + bib_text = bibliographic_text or "No se proporcionó material bibliográfico adicional." + if "[PEGAR TEXTO DEL LIBRO/APUNTE O INDICAR QUE LO SUBISTE COMO ARCHIVO]" in prompt: + prompt = prompt.replace( + "[PEGAR TEXTO DEL LIBRO/APUNTE O INDICAR QUE LO SUBISTE COMO ARCHIVO]", + bib_text + ) + else: + prompt += f"\n\n## Material bibliográfico:\n{bib_text}" + + # Add class number if provided + if class_number is not None: + prompt = prompt.replace("[N]", str(class_number)) + + return prompt + + def get_latex_preamble( + self, + materia: str = "Economía", + class_number: Optional[int] = None + ) -> str: + """ + Get the LaTeX preamble with placeholders replaced. + + Args: + materia: Subject name + class_number: Optional class number + + Returns: + Complete LaTeX preamble as string + """ + preamble = self._load_latex_preamble() + + # Replace placeholders + preamble = preamble.replace("[MATERIA]", materia) + if class_number is not None: + preamble = preamble.replace("[N]", str(class_number)) + + return preamble + + def get_latex_fix_prompt(self, latex_code: str, error_log: str) -> str: + """Get prompt for fixing broken LaTeX code""" + return f"""I have a LaTeX file that failed to compile. Please fix the code. + +COMPILER ERROR LOG: +{error_log[-3000:]} + +BROKEN LATEX CODE: +{latex_code} + +INSTRUCTIONS: +1. Analyze the error log to find the specific syntax error. +2. Fix the LaTeX code. +3. Return ONLY the full corrected LaTeX code. +4. Do not include markdown blocks or explanations. +5. Start immediately with \\documentclass. +6. Ensure all braces {{}} are properly balanced. +7. Ensure all environments \\begin{{...}} have matching \\end{{...}}. +8. Ensure all packages are properly declared. +""" + + def extract_latex_from_response(self, response: str) -> Optional[str]: + """ + Extract clean LaTeX code from AI response. + + Handles cases where AI wraps LaTeX in ```latex...``` blocks. + """ + if not response: + return None + + # Try to find content inside ```latex ... ``` blocks + code_block_pattern = r"```(?:latex|tex)?\s*([\s\S]*?)\s*```" + match = re.search(code_block_pattern, response, re.IGNORECASE) + + if match: + latex = match.group(1).strip() + else: + latex = response.strip() + + # Verify it looks like LaTeX + if "\\documentclass" not in latex: + return None + + # Clean up: remove anything before \documentclass + start_idx = latex.find("\\documentclass") + latex = latex[start_idx:] + + # Clean up: remove anything after \end{document} + if "\\end{document}" in latex: + end_idx = latex.rfind("\\end{document}") + latex = latex[:end_idx + len("\\end{document}")] + + return latex.strip() + + +# Singleton instance for easy import +prompt_manager = PromptManager() diff --git a/services/ai/provider_factory.py b/services/ai/provider_factory.py index 9b6a39f..ee85b82 100644 --- a/services/ai/provider_factory.py +++ b/services/ai/provider_factory.py @@ -3,16 +3,17 @@ AI Provider Factory (Factory Pattern) """ import logging -from typing import Dict, Type +from typing import Dict, Type, Optional from core import AIProcessingError from .base_provider import AIProvider from .claude_provider import ClaudeProvider from .gemini_provider import GeminiProvider +from .parallel_provider import ParallelAIProvider class AIProviderFactory: - """Factory for creating AI providers with fallback""" + """Factory for creating AI providers with fallback and parallel execution""" def __init__(self): self.logger = logging.getLogger(__name__) @@ -20,6 +21,7 @@ class AIProviderFactory: "claude": ClaudeProvider(), "gemini": GeminiProvider(), } + self._parallel_provider: Optional[ParallelAIProvider] = None def get_provider(self, preferred: str = "gemini") -> AIProvider: """Get available provider with fallback""" @@ -50,6 +52,29 @@ class AIProviderFactory: """Get the best available provider (Claude > Gemini)""" return self.get_provider("claude") + def get_parallel_provider(self, max_workers: int = 4) -> ParallelAIProvider: + """Get parallel provider for racing multiple AI providers""" + available = self.get_all_available() + + if not available: + raise AIProcessingError("No providers available for parallel execution") + + if self._parallel_provider is None: + self._parallel_provider = ParallelAIProvider( + providers=available, + max_workers=max_workers + ) + self.logger.info( + f"Created parallel provider with {len(available)} workers: " + f"{', '.join(available.keys())}" + ) + + return self._parallel_provider + + def use_parallel(self) -> bool: + """Check if parallel execution should be used (multiple providers available)""" + return len(self.get_all_available()) > 1 + # Global instance ai_provider_factory = AIProviderFactory() diff --git a/services/webdav_service.py b/services/webdav_service.py index 0d0de76..cb93fa2 100644 --- a/services/webdav_service.py +++ b/services/webdav_service.py @@ -7,8 +7,9 @@ import time import unicodedata import re from pathlib import Path -from typing import Optional, List, Dict +from typing import Optional, List, Dict, Tuple from contextlib import contextmanager +from concurrent.futures import ThreadPoolExecutor, as_completed import requests from requests.auth import HTTPBasicAuth from requests.adapters import HTTPAdapter @@ -107,7 +108,7 @@ class WebDAVService: return self._parse_propfind_response(response.text) def _parse_propfind_response(self, xml_response: str) -> List[str]: - """Parse PROPFIND XML response""" + """Parse PROPFIND XML response and return only files (not directories)""" # Simple parser for PROPFIND response files = [] try: @@ -119,20 +120,41 @@ class WebDAVService: parsed_url = urlparse(settings.NEXTCLOUD_URL) webdav_path = parsed_url.path.rstrip('/') # e.g. /remote.php/webdav - # Find all href elements - for href in root.findall('.//{DAV:}href'): - href_text = href.text or "" - href_text = unquote(href_text) # Decode URL encoding - + # Find all response elements + for response in root.findall('.//{DAV:}response'): + href = response.find('.//{DAV:}href') + if href is None or href.text is None: + continue + + href_text = unquote(href.text) # Decode URL encoding + + # Check if this is a directory (has collection resourcetype) + propstat = response.find('.//{DAV:}propstat') + is_directory = False + if propstat is not None: + prop = propstat.find('.//{DAV:}prop') + if prop is not None: + resourcetype = prop.find('.//{DAV:}resourcetype') + if resourcetype is not None and resourcetype.find('.//{DAV:}collection') is not None: + is_directory = True + + # Skip directories + if is_directory: + continue + + # Also skip paths ending with / (another way to detect directories) + if href_text.endswith('/'): + continue + # Remove base URL from href base_url = settings.NEXTCLOUD_URL.rstrip('/') if href_text.startswith(base_url): href_text = href_text[len(base_url):] - + # Also strip the webdav path if it's there if href_text.startswith(webdav_path): href_text = href_text[len(webdav_path):] - + # Clean up the path href_text = href_text.lstrip('/') if href_text: # Skip empty paths (root directory) @@ -210,6 +232,59 @@ class WebDAVService: except WebDAVError: return False + def upload_batch( + self, + files: List[Tuple[Path, str]], + max_workers: int = 4, + timeout: int = 120 + ) -> Dict[str, bool]: + """ + Upload multiple files concurrently. + + Args: + files: List of (local_path, remote_path) tuples + max_workers: Maximum concurrent uploads + timeout: Timeout per upload in seconds + + Returns: + Dict mapping remote_path to success status + """ + if not files: + return {} + + results: Dict[str, bool] = {} + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + # Submit all upload tasks + future_to_path = { + executor.submit(self.upload, local, remote): remote + for local, remote in files + } + + # Collect results as they complete + for future in as_completed(future_to_path, timeout=timeout): + remote_path = future_to_path[future] + try: + future.result() + results[remote_path] = True + self.logger.info(f"Successfully uploaded: {remote_path}") + except Exception as e: + results[remote_path] = False + self.logger.error(f"Failed to upload {remote_path}: {e}") + + failed_count = sum(1 for success in results.values() if not success) + if failed_count > 0: + self.logger.warning( + f"Batch upload completed with {failed_count} failures " + f"({len(results) - failed_count}/{len(results)} successful)" + ) + else: + self.logger.info( + f"Batch upload completed: {len(results)} files uploaded successfully" + ) + + return results + # Global instance webdav_service = WebDAVService()