Major improvements to AI content generation: ## New Components (app/services/ai/) - PromptLibrary: YAML-based prompt templates with inheritance - ContextEngine: Anti-repetition and best performers tracking - ContentGeneratorV2: Enhanced generation with dynamic parameters - PlatformAdapter: Platform-specific content adaptation - ContentValidator: AI-powered quality scoring (0-100) ## Prompt Library (app/prompts/) - 3 personalities: default, educational, promotional - 5 templates: tip_tech, product_post, service_post, thread, response - 4 platform configs: x, threads, instagram, facebook - Few-shot examples by category: ia, productividad, seguridad ## Database Changes - New table: content_memory (tracks generated content) - New columns in posts: quality_score, score_breakdown, generation_attempts ## New API Endpoints (/api/v2/generate/) - POST /generate - Generation with quality check - POST /generate/batch - Batch generation - POST /quality/evaluate - Evaluate content quality - GET /templates, /personalities, /platforms - List configs ## Celery Tasks - update_engagement_scores (every 6h) - cleanup_old_memory (monthly) - refresh_best_posts_yaml (weekly) ## Tests - Comprehensive tests for all AI engine components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
375 lines
11 KiB
Python
375 lines
11 KiB
Python
"""
|
|
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()
|