""" PlatformAdapter - Adapta contenido según reglas de cada plataforma. Este módulo maneja: - Transformación de contenido base a formato específico de plataforma - Ajuste de longitud, tono, hashtags y formato - Validación de límites de plataforma """ from typing import Dict, Optional, Any, List from dataclasses import dataclass from app.services.ai.prompt_library import prompt_library @dataclass class AdaptedContent: """Contenido adaptado para una plataforma.""" content: str platform: str original_content: str truncated: bool hashtags_adjusted: bool changes_made: List[str] class PlatformAdapter: """ Adaptador de contenido por plataforma. Transforma contenido generado según las reglas específicas de cada red social (X, Threads, Instagram, Facebook). """ def __init__(self): """Inicializar el adaptador.""" self.prompt_lib = prompt_library # Límites rápidos (fallback si no hay YAML) self._default_limits = { "x": {"max_characters": 280, "max_hashtags": 2}, "threads": {"max_characters": 500, "max_hashtags": 5}, "instagram": {"max_characters": 2200, "max_hashtags": 10}, "facebook": {"max_characters": 2000, "max_hashtags": 3}, } def get_limits(self, platform: str) -> Dict[str, int]: """ Obtener límites de una plataforma. Args: platform: Nombre de la plataforma Returns: Dict con límites """ try: return self.prompt_lib.get_platform_limits(platform) except FileNotFoundError: return self._default_limits.get(platform, self._default_limits["x"]) def get_config(self, platform: str) -> Dict: """ Obtener configuración completa de una plataforma. Args: platform: Nombre de la plataforma Returns: Dict con configuración """ try: return self.prompt_lib.get_platform_config(platform) except FileNotFoundError: return {"platform": platform, "limits": self.get_limits(platform)} # === Adaptación Principal === def adapt( self, content: str, platform: str, preserve_hashtags: bool = True ) -> AdaptedContent: """ Adaptar contenido a una plataforma específica. Esta es adaptación basada en reglas (sin IA). Para adaptación con IA, usar adapt_with_ai(). Args: content: Contenido a adaptar platform: Plataforma destino preserve_hashtags: Si preservar hashtags existentes Returns: AdaptedContent con el contenido adaptado """ limits = self.get_limits(platform) config = self.get_config(platform) changes = [] adapted = content truncated = False hashtags_adjusted = False # 1. Extraer y procesar hashtags main_content, hashtags = self._extract_hashtags(adapted) # 2. Ajustar hashtags según plataforma max_hashtags = limits.get("max_hashtags", 5) if len(hashtags) > max_hashtags: hashtags = hashtags[:max_hashtags] hashtags_adjusted = True changes.append(f"Hashtags reducidos a {max_hashtags}") # 3. Verificar y ajustar longitud max_chars = limits.get("max_characters", 2000) # Calcular espacio para hashtags hashtag_space = len(" ".join(hashtags)) + 2 if hashtags else 0 available_for_content = max_chars - hashtag_space if len(main_content) > available_for_content: main_content = self._smart_truncate(main_content, available_for_content) truncated = True changes.append(f"Contenido truncado a {available_for_content} caracteres") # 4. Aplicar formato de plataforma main_content = self._apply_platform_formatting(main_content, platform, config) if main_content != content.replace("#", "").strip(): changes.append("Formato ajustado para plataforma") # 5. Recombinar con hashtags if hashtags and preserve_hashtags: adapted = main_content + "\n\n" + " ".join(hashtags) else: adapted = main_content return AdaptedContent( content=adapted.strip(), platform=platform, original_content=content, truncated=truncated, hashtags_adjusted=hashtags_adjusted, changes_made=changes, ) def adapt_for_all_platforms( self, content: str, platforms: List[str] ) -> Dict[str, AdaptedContent]: """ Adaptar contenido para múltiples plataformas. Args: content: Contenido base platforms: Lista de plataformas Returns: Dict de plataforma -> AdaptedContent """ return { platform: self.adapt(content, platform) for platform in platforms } # === Helpers === def _extract_hashtags(self, content: str) -> tuple[str, List[str]]: """ Extraer hashtags del contenido. Returns: Tuple de (contenido sin hashtags, lista de hashtags) """ import re hashtags = re.findall(r"#\w+", content) # Remover hashtags del contenido main_content = re.sub(r"\s*#\w+", "", content).strip() return main_content, hashtags def _smart_truncate(self, content: str, max_length: int) -> str: """ Truncar contenido de forma inteligente. Intenta cortar en un punto natural (oración, párrafo). Args: content: Contenido a truncar max_length: Longitud máxima Returns: Contenido truncado """ if len(content) <= max_length: return content # Reservar espacio para "..." target_length = max_length - 3 # Intentar cortar en punto/salto de línea truncated = content[:target_length] # Buscar último punto o salto de línea last_period = truncated.rfind(".") last_newline = truncated.rfind("\n") cut_point = max(last_period, last_newline) if cut_point > target_length * 0.5: # Solo si no perdemos mucho return content[:cut_point + 1].strip() # Si no hay buen punto de corte, cortar en última palabra completa last_space = truncated.rfind(" ") if last_space > target_length * 0.7: return content[:last_space].strip() + "..." return truncated + "..." def _apply_platform_formatting( self, content: str, platform: str, config: Dict ) -> str: """ Aplicar formato específico de plataforma. Args: content: Contenido a formatear platform: Plataforma config: Configuración de la plataforma Returns: Contenido formateado """ formatting = config.get("formatting", {}) # Aplicar estilo de bullets si está configurado bullet_style = formatting.get("bullet_style", "•") # Reemplazar bullets genéricos content = content.replace("• ", f"{bullet_style} ") content = content.replace("- ", f"{bullet_style} ") # Ajustar saltos de línea según plataforma if platform == "x": # X: menos saltos de línea, más compacto content = self._compact_line_breaks(content) elif platform == "instagram": # Instagram: más saltos para legibilidad content = self._expand_line_breaks(content) return content def _compact_line_breaks(self, content: str) -> str: """Reducir saltos de línea múltiples a uno.""" import re return re.sub(r"\n{3,}", "\n\n", content) def _expand_line_breaks(self, content: str) -> str: """Asegurar separación entre párrafos.""" import re # Reemplazar un salto por dos donde hay oraciones return re.sub(r"\.(\n)(?=[A-Z])", ".\n\n", content) # === Validación === def validate_for_platform( self, content: str, platform: str ) -> Dict[str, Any]: """ Validar que contenido cumple con límites de plataforma. Args: content: Contenido a validar platform: Plataforma Returns: Dict con resultado de validación """ limits = self.get_limits(platform) _, hashtags = self._extract_hashtags(content) issues = [] # Verificar longitud max_chars = limits.get("max_characters", 2000) if len(content) > max_chars: issues.append({ "type": "length", "message": f"Contenido excede límite ({len(content)}/{max_chars})", "severity": "error" }) # Verificar hashtags max_hashtags = limits.get("max_hashtags", 10) if len(hashtags) > max_hashtags: issues.append({ "type": "hashtags", "message": f"Demasiados hashtags ({len(hashtags)}/{max_hashtags})", "severity": "warning" }) return { "valid": len([i for i in issues if i["severity"] == "error"]) == 0, "issues": issues, "stats": { "characters": len(content), "max_characters": max_chars, "hashtags": len(hashtags), "max_hashtags": max_hashtags, } } # === Generación de Prompts de Adaptación === def get_adaptation_prompt( self, content: str, source_platform: str, target_platform: str ) -> str: """ Generar prompt para adaptar contenido con IA. Args: content: Contenido original source_platform: Plataforma de origen target_platform: Plataforma destino Returns: Prompt para enviar a la IA """ target_config = self.get_config(target_platform) limits = self.get_limits(target_platform) adaptation_rules = target_config.get("adaptation_rules", "") tone_style = target_config.get("tone", {}).get("style", "neutral") prompt = f"""Adapta este contenido de {source_platform} para {target_platform}: CONTENIDO ORIGINAL: {content} LÍMITES DE {target_platform.upper()}: - Máximo caracteres: {limits.get('max_characters', 2000)} - Máximo hashtags: {limits.get('max_hashtags', 5)} TONO PARA {target_platform.upper()}: {tone_style} REGLAS DE ADAPTACIÓN: {adaptation_rules} IMPORTANTE: - Mantén la esencia y mensaje principal - Adapta el tono según la plataforma - Ajusta hashtags apropiadamente - NO inventes información nueva Responde SOLO con el contenido adaptado, sin explicaciones.""" return prompt # Instancia global platform_adapter = PlatformAdapter()