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:
551
app/api/routes/generate.py
Normal file
551
app/api/routes/generate.py
Normal 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
31
app/data/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
269
app/data/content_templates.py
Normal file
269
app/data/content_templates.py
Normal 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"]
|
||||||
@@ -11,7 +11,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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.config import settings
|
||||||
from app.core.database import engine
|
from app.core.database import engine
|
||||||
from app.models import Base
|
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(calendar.router, prefix="/api/calendar", tags=["Calendar"])
|
||||||
app.include_router(interactions.router, prefix="/api/interactions", tags=["Interactions"])
|
app.include_router(interactions.router, prefix="/api/interactions", tags=["Interactions"])
|
||||||
app.include_router(publish.router, prefix="/api/publish", tags=["Publish"])
|
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")
|
@app.get("/api/health")
|
||||||
|
|||||||
326
app/services/batch_generator.py
Normal file
326
app/services/batch_generator.py
Normal 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()
|
||||||
@@ -107,23 +107,23 @@
|
|||||||
|
|
||||||
<!-- AI Assist -->
|
<!-- AI Assist -->
|
||||||
<div class="card p-6">
|
<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">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<button type="button" onclick="generateTip()"
|
<button type="button" onclick="generateTip()"
|
||||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||||
Generar Tip Tech
|
<span>Generar Tip Tech</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="improveContent()"
|
<button type="button" onclick="improveContent()"
|
||||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||||
Mejorar Texto
|
<span>Mejorar Texto</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="adaptContent()"
|
<button type="button" onclick="adaptContent()"
|
||||||
class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
class="ai-btn btn-secondary px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||||
Adaptar por Plataforma
|
<span>Adaptar por Plataforma</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-500 text-sm mt-2">
|
<p id="ai-status" class="text-gray-500 text-sm mt-2">
|
||||||
Usa IA para generar o mejorar el contenido
|
Verificando estado de IA...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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() {
|
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() {
|
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() {
|
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() {
|
function saveDraft() {
|
||||||
@@ -423,7 +589,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load draft on page load
|
// Load draft on page load
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', async () => {
|
||||||
const draftContent = localStorage.getItem('draft_content');
|
const draftContent = localStorage.getItem('draft_content');
|
||||||
const draftPlatforms = localStorage.getItem('draft_platforms');
|
const draftPlatforms = localStorage.getItem('draft_platforms');
|
||||||
|
|
||||||
@@ -437,6 +603,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCharCount();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user