feat(phase-3): Complete AI content generation system

- Add /api/generate endpoints for AI content generation
- Integrate DeepSeek AI into dashboard compose page
- Add content templates for tips, products, services, engagement
- Implement batch generation for weekly/monthly calendars
- Add cost estimation endpoint

New endpoints:
- POST /api/generate/tip - Generate tech tips
- POST /api/generate/product - Generate product posts
- POST /api/generate/service - Generate service posts
- POST /api/generate/thread - Generate educational threads
- POST /api/generate/response - Generate response suggestions
- POST /api/generate/adapt - Adapt content to platform
- POST /api/generate/improve - Improve existing content
- POST /api/generate/batch - Batch generate for calendar
- GET /api/generate/batch/estimate - Estimate batch costs
- GET /api/generate/status - Check AI connection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:49:49 +00:00
parent 3caf2a67fb
commit 964e38564a
6 changed files with 1376 additions and 15 deletions

551
app/api/routes/generate.py Normal file
View File

@@ -0,0 +1,551 @@
"""
API endpoints para generación de contenido con IA.
"""
from typing import Optional, List
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.services.content_generator import content_generator
from app.core.config import settings
router = APIRouter()
# ============================================================
# Schemas
# ============================================================
class GenerateTipRequest(BaseModel):
"""Solicitud para generar tip tech."""
category: str = "general"
platform: str = "x"
template: Optional[str] = None
class GenerateProductPostRequest(BaseModel):
"""Solicitud para generar post de producto."""
product: dict # name, description, price, category, specs, highlights
platform: str = "x"
class GenerateServicePostRequest(BaseModel):
"""Solicitud para generar post de servicio."""
service: dict # name, description, category, target_sectors, benefits, call_to_action
platform: str = "x"
class GenerateThreadRequest(BaseModel):
"""Solicitud para generar hilo educativo."""
topic: str
num_posts: int = 5
class GenerateResponseRequest(BaseModel):
"""Solicitud para generar sugerencias de respuesta."""
interaction_content: str
interaction_type: str = "comment" # comment, mention, dm
context: Optional[str] = None
class AdaptContentRequest(BaseModel):
"""Solicitud para adaptar contenido a plataforma."""
content: str
target_platform: str
class ImproveContentRequest(BaseModel):
"""Solicitud para mejorar contenido existente."""
content: str
platform: str = "x"
style: str = "engaging" # engaging, professional, casual, educational
class GenerateResponse(BaseModel):
"""Respuesta de generación."""
success: bool
content: Optional[str] = None
contents: Optional[List[str]] = None
error: Optional[str] = None
tokens_used: Optional[int] = None
# ============================================================
# Helper
# ============================================================
def check_api_configured():
"""Verificar que la API de DeepSeek esté configurada."""
if not settings.DEEPSEEK_API_KEY:
raise HTTPException(
status_code=503,
detail="DeepSeek API no configurada. Agrega DEEPSEEK_API_KEY en .env"
)
# ============================================================
# Endpoints
# ============================================================
@router.post("/tip", response_model=GenerateResponse)
async def generate_tip(request: GenerateTipRequest):
"""
Generar un tip de tecnología.
Categorías sugeridas:
- productividad, seguridad, ia, programacion, hardware,
- redes, cloud, automatizacion, impresion3d, general
"""
check_api_configured()
try:
content = await content_generator.generate_tip_tech(
category=request.category,
platform=request.platform,
template=request.template
)
return GenerateResponse(
success=True,
content=content
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/product", response_model=GenerateResponse)
async def generate_product_post(request: GenerateProductPostRequest):
"""
Generar post promocional de producto.
Campos del producto:
- name (requerido): Nombre del producto
- description: Descripción
- price (requerido): Precio en MXN
- category: Categoría (computadoras, impresoras3d, accesorios)
- specs: Dict con especificaciones técnicas
- highlights: Lista de puntos destacados
"""
check_api_configured()
# Validar campos mínimos
if "name" not in request.product or "price" not in request.product:
raise HTTPException(
status_code=400,
detail="El producto debe tener 'name' y 'price'"
)
try:
content = await content_generator.generate_product_post(
product=request.product,
platform=request.platform
)
return GenerateResponse(
success=True,
content=content
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/service", response_model=GenerateResponse)
async def generate_service_post(request: GenerateServicePostRequest):
"""
Generar post promocional de servicio.
Campos del servicio:
- name (requerido): Nombre del servicio
- description: Descripción
- category: Categoría (consultoria, desarrollo, soporte, capacitacion)
- target_sectors: Lista de sectores objetivo
- benefits: Lista de beneficios
- call_to_action: CTA personalizado
"""
check_api_configured()
if "name" not in request.service:
raise HTTPException(
status_code=400,
detail="El servicio debe tener 'name'"
)
try:
content = await content_generator.generate_service_post(
service=request.service,
platform=request.platform
)
return GenerateResponse(
success=True,
content=content
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/thread", response_model=GenerateResponse)
async def generate_thread(request: GenerateThreadRequest):
"""
Generar hilo educativo.
- topic: Tema del hilo (ej: "5 tips para proteger tu PC")
- num_posts: Cantidad de posts en el hilo (3-10)
"""
check_api_configured()
if request.num_posts < 3 or request.num_posts > 10:
raise HTTPException(
status_code=400,
detail="num_posts debe estar entre 3 y 10"
)
try:
posts = await content_generator.generate_thread(
topic=request.topic,
num_posts=request.num_posts
)
return GenerateResponse(
success=True,
contents=posts
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/response", response_model=GenerateResponse)
async def generate_response_suggestions(request: GenerateResponseRequest):
"""
Generar sugerencias de respuesta para una interacción.
Devuelve 3 opciones de respuesta diferentes.
"""
check_api_configured()
try:
suggestions = await content_generator.generate_response_suggestion(
interaction_content=request.interaction_content,
interaction_type=request.interaction_type,
context=request.context
)
return GenerateResponse(
success=True,
contents=suggestions
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/adapt", response_model=GenerateResponse)
async def adapt_content(request: AdaptContentRequest):
"""
Adaptar contenido existente a una plataforma específica.
Útil para tomar contenido de una plataforma y adaptarlo
a los límites y estilo de otra.
"""
check_api_configured()
valid_platforms = ["x", "threads", "instagram", "facebook"]
if request.target_platform.lower() not in valid_platforms:
raise HTTPException(
status_code=400,
detail=f"Plataforma no válida. Opciones: {valid_platforms}"
)
try:
content = await content_generator.adapt_content_for_platform(
content=request.content,
target_platform=request.target_platform.lower()
)
return GenerateResponse(
success=True,
content=content
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.post("/improve", response_model=GenerateResponse)
async def improve_content(request: ImproveContentRequest):
"""
Mejorar contenido existente.
Estilos disponibles:
- engaging: Más atractivo y con gancho
- professional: Más formal y corporativo
- casual: Más cercano y amigable
- educational: Más informativo y didáctico
"""
check_api_configured()
try:
# Usar el generador para mejorar
from openai import OpenAI
client = OpenAI(
api_key=settings.DEEPSEEK_API_KEY,
base_url=settings.DEEPSEEK_BASE_URL
)
style_prompts = {
"engaging": "Hazlo más atractivo, con un gancho inicial que capture atención",
"professional": "Hazlo más profesional y corporativo, manteniendo credibilidad",
"casual": "Hazlo más cercano y amigable, como si hablaras con un amigo",
"educational": "Hazlo más informativo y didáctico, que enseñe algo útil"
}
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 1000
}
prompt = f"""Mejora este contenido para {request.platform}:
CONTENIDO ORIGINAL:
{request.content}
ESTILO DESEADO: {style_prompts.get(request.style, style_prompts['engaging'])}
LÍMITE DE CARACTERES: {char_limits.get(request.platform, 500)}
REQUISITOS:
- Mantén el mensaje principal
- Mejora la redacción y el impacto
- Incluye emojis relevantes
- Termina con hashtags apropiados
Responde SOLO con el contenido mejorado."""
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": content_generator._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=400,
temperature=0.7
)
improved = response.choices[0].message.content.strip()
return GenerateResponse(
success=True,
content=improved,
tokens_used=response.usage.total_tokens
)
except Exception as e:
return GenerateResponse(
success=False,
error=str(e)
)
@router.get("/categories")
async def get_tip_categories():
"""Obtener categorías disponibles para tips."""
return {
"categories": [
{"id": "productividad", "name": "Productividad", "description": "Tips para ser más eficiente"},
{"id": "seguridad", "name": "Seguridad", "description": "Ciberseguridad y protección"},
{"id": "ia", "name": "Inteligencia Artificial", "description": "IA y machine learning"},
{"id": "programacion", "name": "Programación", "description": "Desarrollo de software"},
{"id": "hardware", "name": "Hardware", "description": "Equipos y componentes"},
{"id": "redes", "name": "Redes", "description": "Networking y conectividad"},
{"id": "cloud", "name": "Cloud", "description": "Servicios en la nube"},
{"id": "automatizacion", "name": "Automatización", "description": "Automatizar procesos"},
{"id": "impresion3d", "name": "Impresión 3D", "description": "Impresoras y diseño 3D"},
{"id": "general", "name": "General", "description": "Tips variados de tecnología"},
]
}
class BatchGenerateRequest(BaseModel):
"""Solicitud de generación por lotes."""
platforms: List[str]
days: int = 7 # 7 para semana, 30 para mes
tip_categories: Optional[List[str]] = None
@router.post("/batch")
async def generate_batch(request: BatchGenerateRequest):
"""
Generar contenido por lotes para el calendario.
- **platforms**: Lista de plataformas ["x", "threads", "instagram"]
- **days**: Cantidad de días (7 para semana, 30 para mes)
- **tip_categories**: Categorías de tips a usar
"""
check_api_configured()
if request.days < 1 or request.days > 30:
raise HTTPException(
status_code=400,
detail="days debe estar entre 1 y 30"
)
from app.services.batch_generator import batch_generator
try:
result = await batch_generator._generate_batch(
platforms=request.platforms,
start_date=None, # Usa mañana por defecto
days=request.days,
tip_categories=request.tip_categories
)
return {
"success": result.success,
"total_requested": result.total_requested,
"total_generated": result.total_generated,
"posts": [
{
"content": p.content,
"content_type": p.content_type.value,
"platform": p.platform,
"scheduled_at": p.scheduled_at.isoformat(),
"metadata": p.metadata
}
for p in result.posts
],
"errors": result.errors
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
@router.get("/batch/estimate")
async def estimate_batch_cost(
platforms: str, # comma-separated
days: int = 7
):
"""
Estimar costo de generación por lotes.
Calcula tokens y costo estimado sin generar contenido.
"""
from app.services.batch_generator import batch_generator
platform_list = [p.strip() for p in platforms.split(",")]
estimate = batch_generator.calculate_costs(platform_list, days)
return estimate
@router.post("/tips/batch")
async def generate_tips_batch(
count: int = 10,
platform: str = "x",
categories: Optional[str] = None # comma-separated
):
"""
Generar múltiples tips de una vez.
- **count**: Cantidad de tips (1-20)
- **platform**: Plataforma destino
- **categories**: Categorías separadas por coma
"""
check_api_configured()
if count < 1 or count > 20:
raise HTTPException(
status_code=400,
detail="count debe estar entre 1 y 20"
)
from app.services.batch_generator import batch_generator
category_list = None
if categories:
category_list = [c.strip() for c in categories.split(",")]
try:
tips = await batch_generator.generate_tips_batch(
count=count,
platform=platform,
categories=category_list
)
return {
"success": True,
"count": len(tips),
"tips": tips
}
except Exception as e:
return {
"success": False,
"error": str(e),
"tips": []
}
@router.get("/status")
async def get_ai_status():
"""Verificar estado de la API de IA."""
configured = bool(settings.DEEPSEEK_API_KEY)
result = {
"configured": configured,
"provider": "DeepSeek",
"model": "deepseek-chat",
"base_url": settings.DEEPSEEK_BASE_URL
}
if configured:
try:
# Test rápido
from openai import OpenAI
client = OpenAI(
api_key=settings.DEEPSEEK_API_KEY,
base_url=settings.DEEPSEEK_BASE_URL
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "test"}],
max_tokens=5
)
result["status"] = "connected"
result["test"] = "OK"
except Exception as e:
result["status"] = "error"
result["error"] = str(e)
else:
result["status"] = "not_configured"
return result

31
app/data/__init__.py Normal file
View File

@@ -0,0 +1,31 @@
"""
Data module - templates and static data for content generation.
"""
from app.data.content_templates import (
TIP_TEMPLATES,
PRODUCT_TEMPLATES,
SERVICE_TEMPLATES,
ENGAGEMENT_TEMPLATES,
THREAD_TEMPLATES,
OPTIMAL_POSTING_TIMES,
get_tip_template,
get_product_template,
get_service_template,
get_engagement_template,
get_optimal_times,
)
__all__ = [
"TIP_TEMPLATES",
"PRODUCT_TEMPLATES",
"SERVICE_TEMPLATES",
"ENGAGEMENT_TEMPLATES",
"THREAD_TEMPLATES",
"OPTIMAL_POSTING_TIMES",
"get_tip_template",
"get_product_template",
"get_service_template",
"get_engagement_template",
"get_optimal_times",
]

View File

@@ -0,0 +1,269 @@
"""
Plantillas predefinidas para generación de contenido.
Estas plantillas guían a la IA para generar contenido consistente.
"""
# ============================================================
# TIPS TECH
# ============================================================
TIP_TEMPLATES = {
"productividad": [
"Tip de productividad: [herramienta/técnica] puede ahorrarte [tiempo/esfuerzo] al [tarea]. #Productividad",
"¿Sabías que [atajo/función] en [app/sistema] puede [beneficio]? #TipTech",
"3 segundos que te ahorran 30 minutos: [tip rápido]. #Eficiencia",
],
"seguridad": [
"Tip de seguridad: [acción] para proteger [recurso]. Nunca [error común]. #Ciberseguridad",
"¿Tu [dispositivo/cuenta] está protegido? Verifica que tengas [medida de seguridad]. #SeguridadDigital",
"Error común de seguridad: [error]. Solución: [solución]. #InfoSec",
],
"ia": [
"Prompt tip: Para mejores resultados con IA, [técnica]. #IAparaNegocios",
"Herramienta IA del día: [herramienta] te ayuda a [beneficio]. #InteligenciaArtificial",
"Cómo uso IA para [tarea]: [paso breve]. #ProductividadIA",
],
"programacion": [
"Code tip: [técnica/patrón] mejora [aspecto] en tu código. #DevTips",
"Atajo en [IDE/lenguaje]: [atajo] → [resultado]. #ProgrammingTips",
"Evita este error común en [tecnología]: [error] → [solución]. #CodeQuality",
],
"hardware": [
"Tip de hardware: [acción] para [beneficio] en tu [equipo]. #TechTips",
"¿Tu PC está lenta? Prueba [solución]. #Mantenimiento",
"Antes de comprar [componente], verifica [aspecto]. #CompraTech",
],
"impresion3d": [
"Tip 3D: [configuración/técnica] mejora [aspecto] de tus impresiones. #Impresion3D",
"Material del día: [filamento] es ideal para [uso]. #3DPrinting",
"Error común en 3D: [problema] → Solución: [solución]. #Makers",
],
"general": [
"Tip tech del día: [consejo general]. #Tecnologia",
"¿Conocías este truco? [tip]. #TechTips",
"Tecnología que facilita tu día: [herramienta/técnica]. #Tech",
]
}
# ============================================================
# PRODUCTOS
# ============================================================
PRODUCT_TEMPLATES = {
"computadoras": """
🖥️ {name}
{highlight_1}
{highlight_2}
{highlight_3}
💰 ${price:,.0f} MXN
{call_to_action}
#Computadoras #Tech #Tijuana
""",
"laptops": """
💻 {name}
{highlight_1}
{highlight_2}
{highlight_3}
Precio: ${price:,.0f} MXN
{call_to_action}
#Laptops #Portátiles #Tech
""",
"impresoras3d": """
🔧 {name}
Lo que puedes crear:
• Prototipos
• Piezas personalizadas
• Figuras y modelos
{specs_summary}
💰 ${price:,.0f} MXN
{call_to_action}
#Impresion3D #3DPrinting #Makers
""",
"accesorios": """
{emoji} {name}
{description}
${price:,.0f} MXN
{call_to_action}
#Accesorios #Tech
""",
}
# ============================================================
# SERVICIOS
# ============================================================
SERVICE_TEMPLATES = {
"consultoria": """
🎯 {name}
¿{pain_point}?
Te ayudamos a:
{benefit_1}
{benefit_2}
{benefit_3}
{call_to_action}
#ConsultoríaTech #TransformaciónDigital
""",
"desarrollo": """
💻 {name}
Desarrollamos soluciones a medida:
{feature_1}
{feature_2}
{feature_3}
{call_to_action}
#DesarrolloSoftware #Automatización
""",
"soporte": """
🛠️ {name}
Problemas técnicos resueltos:
{issue_1}
{issue_2}
{issue_3}
{call_to_action}
#SoporteTécnico #IT #Tijuana
""",
"capacitacion": """
📚 {name}
Aprende:
🎓 {topic_1}
🎓 {topic_2}
🎓 {topic_3}
{call_to_action}
#Capacitación #TechTraining
""",
}
# ============================================================
# ENGAGEMENT
# ============================================================
ENGAGEMENT_TEMPLATES = {
"pregunta": [
"🤔 ¿Cuál es tu mayor reto con [tema]? Cuéntanos en los comentarios.",
"Pregunta del día: ¿[pregunta relevante]? 👇",
"Si pudieras automatizar UNA tarea de tu trabajo, ¿cuál sería? 🤖",
],
"encuesta": [
"¿Qué prefieres?\n\n🅰️ [Opción A]\n🅱️ [Opción B]\n\nResponde con el emoji.",
"Tu herramienta favorita para [tarea]:\n\n1⃣ [Op1]\n2⃣ [Op2]\n3⃣ [Op3]\n4⃣ Otra (comenta)",
],
"behind_scenes": [
"Un día normal en @ConsultoriaAS: [actividad]. ¿Qué les gustaría ver? 👀",
"Trabajando en [proyecto]. Pronto más detalles... 🔜",
],
"celebracion": [
"🎉 [Logro/Milestone]. ¡Gracias a todos los que confían en nosotros!",
"Otro [proyecto/cliente] satisfecho. Esto es lo que nos motiva 💪",
],
}
# ============================================================
# HILOS EDUCATIVOS
# ============================================================
THREAD_TEMPLATES = {
"tutorial": {
"hook": "🧵 [Título llamativo en forma de promesa]\n\nEn este hilo te explico paso a paso 👇",
"content_format": "{number}/ {step_title}\n\n{explanation}",
"closing": "📌 Guarda este hilo para cuando lo necesites.\n\n¿Te fue útil? RT para que llegue a más personas.\n\n#[hashtag1] #[hashtag2]"
},
"tips_list": {
"hook": "🧵 {count} tips de {topic} que ojalá hubiera sabido antes:\n\n👇",
"content_format": "{number}/ {tip}",
"closing": "¿Cuál agregarías tú?\n\nSíguenos para más tips como estos.\n\n#[hashtag1] #[hashtag2]"
},
"comparison": {
"hook": "🧵 {option_a} vs {option_b}: ¿Cuál elegir?\n\nTe lo explico 👇",
"content_format": "{number}/ {aspect}:\n\n{option_a}: {value_a}\n{option_b}: {value_b}",
"closing": "Mi recomendación: {recommendation}\n\n¿Con cuál te quedas?\n\n#[hashtag1] #[hashtag2]"
},
}
# ============================================================
# HORARIOS ÓPTIMOS
# ============================================================
OPTIMAL_POSTING_TIMES = {
"x": {
"weekday": ["09:00", "12:00", "17:00", "20:00"],
"weekend": ["10:00", "14:00", "19:00"],
"best": "12:00"
},
"threads": {
"weekday": ["08:00", "12:00", "18:00"],
"weekend": ["10:00", "15:00"],
"best": "18:00"
},
"instagram": {
"weekday": ["11:00", "14:00", "19:00"],
"weekend": ["10:00", "14:00", "20:00"],
"best": "19:00"
},
"facebook": {
"weekday": ["09:00", "13:00", "16:00"],
"weekend": ["12:00", "15:00"],
"best": "13:00"
},
}
# ============================================================
# HELPERS
# ============================================================
def get_tip_template(category: str) -> str:
"""Obtener plantilla aleatoria para un tip."""
import random
templates = TIP_TEMPLATES.get(category, TIP_TEMPLATES["general"])
return random.choice(templates)
def get_product_template(category: str) -> str:
"""Obtener plantilla para producto."""
return PRODUCT_TEMPLATES.get(category, PRODUCT_TEMPLATES["accesorios"])
def get_service_template(category: str) -> str:
"""Obtener plantilla para servicio."""
return SERVICE_TEMPLATES.get(category, SERVICE_TEMPLATES["consultoria"])
def get_engagement_template(template_type: str) -> str:
"""Obtener plantilla de engagement."""
import random
templates = ENGAGEMENT_TEMPLATES.get(template_type, ENGAGEMENT_TEMPLATES["pregunta"])
return random.choice(templates)
def get_optimal_times(platform: str, is_weekend: bool = False) -> list:
"""Obtener horarios óptimos para publicar."""
times = OPTIMAL_POSTING_TIMES.get(platform, OPTIMAL_POSTING_TIMES["x"])
return times["weekend"] if is_weekend else times["weekday"]

View File

@@ -11,7 +11,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish
from app.api.routes import posts, products, services, calendar, dashboard, interactions, auth, publish, generate
from app.core.config import settings
from app.core.database import engine
from app.models import Base
@@ -63,6 +63,7 @@ app.include_router(services.router, prefix="/api/services", tags=["Services"])
app.include_router(calendar.router, prefix="/api/calendar", tags=["Calendar"])
app.include_router(interactions.router, prefix="/api/interactions", tags=["Interactions"])
app.include_router(publish.router, prefix="/api/publish", tags=["Publish"])
app.include_router(generate.router, prefix="/api/generate", tags=["AI Generation"])
@app.get("/api/health")

View File

@@ -0,0 +1,326 @@
"""
Servicio de generación de contenido por lotes.
Genera múltiples posts para el calendario de contenido.
"""
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum
from app.services.content_generator import content_generator
from app.data.content_templates import (
get_tip_template,
get_optimal_times,
OPTIMAL_POSTING_TIMES
)
from app.core.config import settings
class ContentType(str, Enum):
TIP = "tip"
PRODUCT = "product"
SERVICE = "service"
ENGAGEMENT = "engagement"
THREAD = "thread"
@dataclass
class GeneratedPost:
"""Post generado para el calendario."""
content: str
content_type: ContentType
platform: str
scheduled_at: datetime
metadata: Dict = None
@dataclass
class BatchResult:
"""Resultado de generación por lotes."""
success: bool
posts: List[GeneratedPost]
errors: List[str]
total_requested: int
total_generated: int
class BatchGenerator:
"""Generador de contenido por lotes."""
# Distribución de contenido por defecto (porcentaje)
DEFAULT_DISTRIBUTION = {
ContentType.TIP: 60,
ContentType.PRODUCT: 20,
ContentType.SERVICE: 15,
ContentType.ENGAGEMENT: 5,
}
# Frecuencia por plataforma (posts por día)
DEFAULT_FREQUENCY = {
"x": 4,
"threads": 3,
"instagram": 2,
"facebook": 1,
}
def __init__(self):
self.generator = content_generator
async def generate_week(
self,
platforms: List[str],
start_date: Optional[datetime] = None,
distribution: Optional[Dict[ContentType, int]] = None,
frequency: Optional[Dict[str, int]] = None,
tip_categories: Optional[List[str]] = None,
) -> BatchResult:
"""
Generar contenido para una semana.
Args:
platforms: Lista de plataformas
start_date: Fecha de inicio (default: mañana)
distribution: Distribución de tipos de contenido
frequency: Posts por día por plataforma
tip_categories: Categorías para tips
Returns:
BatchResult con posts generados
"""
if start_date is None:
start_date = datetime.now().replace(
hour=0, minute=0, second=0, microsecond=0
) + timedelta(days=1)
return await self._generate_batch(
platforms=platforms,
start_date=start_date,
days=7,
distribution=distribution,
frequency=frequency,
tip_categories=tip_categories
)
async def generate_month(
self,
platforms: List[str],
start_date: Optional[datetime] = None,
distribution: Optional[Dict[ContentType, int]] = None,
frequency: Optional[Dict[str, int]] = None,
tip_categories: Optional[List[str]] = None,
) -> BatchResult:
"""Generar contenido para un mes (30 días)."""
if start_date is None:
start_date = datetime.now().replace(
hour=0, minute=0, second=0, microsecond=0
) + timedelta(days=1)
return await self._generate_batch(
platforms=platforms,
start_date=start_date,
days=30,
distribution=distribution,
frequency=frequency,
tip_categories=tip_categories
)
async def _generate_batch(
self,
platforms: List[str],
start_date: datetime,
days: int,
distribution: Optional[Dict[ContentType, int]] = None,
frequency: Optional[Dict[str, int]] = None,
tip_categories: Optional[List[str]] = None,
) -> BatchResult:
"""Generar lote de posts."""
distribution = distribution or self.DEFAULT_DISTRIBUTION
frequency = frequency or self.DEFAULT_FREQUENCY
tip_categories = tip_categories or [
"productividad", "seguridad", "ia", "programacion",
"hardware", "impresion3d", "general"
]
posts = []
errors = []
total_requested = 0
# Calcular posts necesarios por plataforma
for platform in platforms:
posts_per_day = frequency.get(platform, 2)
total_posts = posts_per_day * days
total_requested += total_posts
# Distribuir por tipo
posts_by_type = {}
for content_type, percentage in distribution.items():
count = int(total_posts * percentage / 100)
posts_by_type[content_type] = count
# Generar posts para cada tipo
current_date = start_date
day_counter = 0
for content_type, count in posts_by_type.items():
for i in range(count):
# Calcular fecha y hora
day_offset = (i * days) // count
post_date = start_date + timedelta(days=day_offset)
is_weekend = post_date.weekday() >= 5
# Obtener hora óptima
times = get_optimal_times(platform, is_weekend)
time_index = i % len(times)
hour, minute = map(int, times[time_index].split(":"))
scheduled_at = post_date.replace(hour=hour, minute=minute)
try:
# Generar contenido
content = await self._generate_content(
content_type=content_type,
platform=platform,
tip_categories=tip_categories
)
if content:
posts.append(GeneratedPost(
content=content,
content_type=content_type,
platform=platform,
scheduled_at=scheduled_at,
metadata={
"batch_generated": True,
"category": tip_categories[i % len(tip_categories)]
if content_type == ContentType.TIP else None
}
))
except Exception as e:
errors.append(f"{platform}/{content_type}: {str(e)}")
return BatchResult(
success=len(posts) > 0,
posts=posts,
errors=errors,
total_requested=total_requested,
total_generated=len(posts)
)
async def _generate_content(
self,
content_type: ContentType,
platform: str,
tip_categories: List[str]
) -> Optional[str]:
"""Generar contenido individual."""
import random
if content_type == ContentType.TIP:
category = random.choice(tip_categories)
template = get_tip_template(category)
return await self.generator.generate_tip_tech(
category=category,
platform=platform,
template=template
)
elif content_type == ContentType.ENGAGEMENT:
# Generar pregunta de engagement
prompts = [
"¿Cuál es tu mayor reto tecnológico esta semana?",
"¿Qué herramienta de productividad no podrías dejar de usar?",
"¿Prefieres trabajar en oficina o remoto? ¿Por qué?",
"¿Qué tecnología te gustaría aprender este año?",
"¿Cuál es tu consejo #1 para mantenerte productivo?",
]
return random.choice(prompts)
# Para productos y servicios, devolver None
# (requieren datos específicos)
return None
async def generate_tips_batch(
self,
count: int,
platform: str,
categories: Optional[List[str]] = None
) -> List[str]:
"""
Generar múltiples tips de una vez.
Args:
count: Cantidad de tips a generar
platform: Plataforma destino
categories: Categorías a usar (se rotarán)
Returns:
Lista de tips generados
"""
categories = categories or [
"productividad", "seguridad", "ia", "programacion",
"hardware", "general"
]
tips = []
for i in range(count):
category = categories[i % len(categories)]
try:
tip = await self.generator.generate_tip_tech(
category=category,
platform=platform
)
tips.append(tip)
except Exception:
pass # Ignorar errores individuales
# Pequeña pausa para no saturar la API
if i < count - 1:
await asyncio.sleep(0.5)
return tips
def calculate_costs(
self,
platforms: List[str],
days: int,
frequency: Optional[Dict[str, int]] = None
) -> Dict:
"""
Calcular costos estimados para un lote.
Returns:
Dict con estimaciones de tokens y costos
"""
frequency = frequency or self.DEFAULT_FREQUENCY
total_posts = sum(
frequency.get(p, 2) * days
for p in platforms
)
# Estimaciones basadas en análisis previo
avg_input_tokens = 650
avg_output_tokens = 250
total_input = total_posts * avg_input_tokens
total_output = total_posts * avg_output_tokens
# Precios DeepSeek
input_cost = total_input * 0.14 / 1_000_000
output_cost = total_output * 0.28 / 1_000_000
return {
"total_posts": total_posts,
"estimated_input_tokens": total_input,
"estimated_output_tokens": total_output,
"estimated_cost_usd": round(input_cost + output_cost, 4),
"platforms": platforms,
"days": days,
"frequency": frequency
}
# Instancia global
batch_generator = BatchGenerator()

View File

@@ -107,23 +107,23 @@
<!-- AI Assist -->
<div class="card p-6">
<h3 class="font-semibold mb-4">Asistente IA</h3>
<h3 class="font-semibold mb-4">Asistente IA (DeepSeek)</h3>
<div class="flex gap-2 flex-wrap">
<button type="button" onclick="generateTip()"
class="btn-secondary px-4 py-2 rounded-lg text-sm">
Generar Tip Tech
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<span>Generar Tip Tech</span>
</button>
<button type="button" onclick="improveContent()"
class="btn-secondary px-4 py-2 rounded-lg text-sm">
Mejorar Texto
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<span>Mejorar Texto</span>
</button>
<button type="button" onclick="adaptContent()"
class="btn-secondary px-4 py-2 rounded-lg text-sm">
Adaptar por Plataforma
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
<span>Adaptar por Plataforma</span>
</button>
</div>
<p class="text-gray-500 text-sm mt-2">
Usa IA para generar o mejorar el contenido
<p id="ai-status" class="text-gray-500 text-sm mt-2">
Verificando estado de IA...
</p>
</div>
@@ -402,17 +402,183 @@
}
});
// AI functions (placeholder - will connect to backend)
// AI functions
let aiLoading = false;
function setAiLoading(loading) {
aiLoading = loading;
document.querySelectorAll('.ai-btn').forEach(btn => {
btn.disabled = loading;
if (loading) {
btn.classList.add('opacity-50', 'cursor-wait');
} else {
btn.classList.remove('opacity-50', 'cursor-wait');
}
});
}
async function checkAiStatus() {
try {
const response = await fetch('/api/generate/status');
const data = await response.json();
return data.configured && data.status === 'connected';
} catch {
return false;
}
}
async function generateTip() {
alert('Función de generación con IA - Requiere configurar DEEPSEEK_API_KEY');
if (aiLoading) return;
// Show category selector
const categories = [
'productividad', 'seguridad', 'ia', 'programacion',
'hardware', 'redes', 'cloud', 'automatizacion', 'impresion3d', 'general'
];
const category = prompt(
'Categoría del tip:\n\n' + categories.join(', ') + '\n\n(Enter para "general")'
) || 'general';
const platform = selectedPlatforms[0] || 'x';
setAiLoading(true);
try {
const response = await fetch('/api/generate/tip', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, platform })
});
const data = await response.json();
if (data.success) {
document.getElementById('content').value = data.content;
updateCharCount();
} else {
alert('Error: ' + (data.error || 'No se pudo generar'));
}
} catch (error) {
alert('Error de conexión: ' + error.message);
} finally {
setAiLoading(false);
}
}
async function improveContent() {
alert('Función de mejora con IA - Requiere configurar DEEPSEEK_API_KEY');
if (aiLoading) return;
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Primero escribe algo de contenido para mejorar');
return;
}
const styles = ['engaging', 'professional', 'casual', 'educational'];
const style = prompt(
'Estilo de mejora:\n\n' +
'- engaging: Más atractivo\n' +
'- professional: Más formal\n' +
'- casual: Más cercano\n' +
'- educational: Más didáctico\n\n' +
'(Enter para "engaging")'
) || 'engaging';
const platform = selectedPlatforms[0] || 'x';
setAiLoading(true);
try {
const response = await fetch('/api/generate/improve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, platform, style })
});
const data = await response.json();
if (data.success) {
document.getElementById('content').value = data.content;
updateCharCount();
} else {
alert('Error: ' + (data.error || 'No se pudo mejorar'));
}
} catch (error) {
alert('Error de conexión: ' + error.message);
} finally {
setAiLoading(false);
}
}
async function adaptContent() {
alert('Función de adaptación con IA - Requiere configurar DEEPSEEK_API_KEY');
if (aiLoading) return;
const content = document.getElementById('content').value;
if (!content.trim()) {
alert('Primero escribe algo de contenido para adaptar');
return;
}
if (selectedPlatforms.length < 2) {
alert('Selecciona al menos 2 plataformas para adaptar el contenido');
return;
}
setAiLoading(true);
try {
// Adaptar a cada plataforma excepto la primera (que es el contenido original)
const adaptedContent = { [selectedPlatforms[0]]: content };
for (let i = 1; i < selectedPlatforms.length; i++) {
const platform = selectedPlatforms[i];
const response = await fetch('/api/generate/adapt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, target_platform: platform })
});
const data = await response.json();
if (data.success) {
adaptedContent[platform] = data.content;
}
}
// Mostrar resultados en preview
const previewDiv = document.getElementById('preview-content');
let html = '<p class="text-gray-400 mb-4">Contenido adaptado por plataforma:</p>';
for (const [platform, text] of Object.entries(adaptedContent)) {
html += `
<div class="bg-gray-800 rounded-lg p-4 mb-2">
<div class="font-semibold mb-2">${platform.toUpperCase()}</div>
<p class="text-gray-300 whitespace-pre-wrap text-sm">${text}</p>
<button onclick="useAdaptedContent('${platform}', this)"
class="mt-2 text-amber-500 text-sm hover:underline"
data-content="${encodeURIComponent(text)}">
Usar este
</button>
</div>
`;
}
previewDiv.innerHTML = html;
document.getElementById('preview-modal').classList.remove('hidden');
document.getElementById('preview-modal').classList.add('flex');
} catch (error) {
alert('Error de conexión: ' + error.message);
} finally {
setAiLoading(false);
}
}
function useAdaptedContent(platform, btn) {
const content = decodeURIComponent(btn.dataset.content);
document.getElementById('content').value = content;
updateCharCount();
closePreview();
}
function saveDraft() {
@@ -423,7 +589,7 @@
}
// Load draft on page load
window.addEventListener('load', () => {
window.addEventListener('load', async () => {
const draftContent = localStorage.getItem('draft_content');
const draftPlatforms = localStorage.getItem('draft_platforms');
@@ -437,6 +603,23 @@
}
updateCharCount();
// Check AI status
const aiStatus = document.getElementById('ai-status');
try {
const response = await fetch('/api/generate/status');
const data = await response.json();
if (data.configured && data.status === 'connected') {
aiStatus.innerHTML = '<span class="text-green-400">IA conectada y lista</span>';
} else if (data.configured) {
aiStatus.innerHTML = '<span class="text-yellow-400">IA configurada pero con error: ' + (data.error || 'desconocido') + '</span>';
} else {
aiStatus.innerHTML = '<span class="text-red-400">IA no configurada. Agrega DEEPSEEK_API_KEY en .env</span>';
}
} catch (error) {
aiStatus.innerHTML = '<span class="text-red-400">No se pudo verificar estado de IA</span>';
}
});
</script>
</body>