Implementación inicial del sistema de automatización de redes sociales

- Estructura completa del proyecto con FastAPI
- Modelos de base de datos (productos, servicios, posts, calendario, interacciones)
- Publishers para X, Threads, Instagram, Facebook
- Generador de contenido con DeepSeek API
- Worker de Celery con tareas programadas
- Dashboard básico con templates HTML
- Docker Compose para despliegue
- Documentación completa

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 01:11:44 +00:00
commit 049d2133f9
53 changed files with 5876 additions and 0 deletions

1
app/services/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Services module

View File

@@ -0,0 +1,314 @@
"""
Servicio de generación de contenido con DeepSeek API.
"""
import json
from typing import Optional, List, Dict
from openai import OpenAI
from app.core.config import settings
class ContentGenerator:
"""Generador de contenido usando DeepSeek API."""
def __init__(self):
self.client = OpenAI(
api_key=settings.DEEPSEEK_API_KEY,
base_url=settings.DEEPSEEK_BASE_URL
)
self.model = "deepseek-chat"
def _get_system_prompt(self) -> str:
"""Obtener el prompt del sistema con la personalidad de la marca."""
return f"""Eres el Community Manager de {settings.BUSINESS_NAME}, una empresa de tecnología ubicada en {settings.BUSINESS_LOCATION}.
SOBRE LA EMPRESA:
- Especializada en soluciones de IA, automatización y transformación digital
- Vende equipos de cómputo e impresoras 3D
- Sitio web: {settings.BUSINESS_WEBSITE}
TONO DE COMUNICACIÓN:
{settings.CONTENT_TONE}
ESTILO (inspirado en @midudev, @MoureDev, @SoyDalto):
- Tips cortos y accionables
- Contenido educativo de valor
- Cercano pero profesional
- Uso moderado de emojis
- Hashtags relevantes (máximo 3-5)
REGLAS:
- Nunca uses lenguaje ofensivo
- No hagas promesas exageradas
- Sé honesto y transparente
- Enfócate en ayudar, no en vender directamente
- Adapta el contenido a cada plataforma"""
async def generate_tip_tech(
self,
category: str,
platform: str,
template: Optional[str] = None
) -> str:
"""Generar un tip tech."""
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 500
}
prompt = f"""Genera un tip de tecnología para la categoría: {category}
PLATAFORMA: {platform}
LÍMITE DE CARACTERES: {char_limits.get(platform, 500)}
{f'USA ESTE TEMPLATE COMO BASE: {template}' if template else ''}
REQUISITOS:
- Tip práctico y accionable
- Fácil de entender
- Incluye un emoji relevante al inicio
- Termina con 2-3 hashtags relevantes
- NO incluyas enlaces
Responde SOLO con el texto del post, sin explicaciones."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=300,
temperature=0.7
)
return response.choices[0].message.content.strip()
async def generate_product_post(
self,
product: Dict,
platform: str
) -> str:
"""Generar post para un producto."""
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 1000
}
prompt = f"""Genera un post promocional para este producto:
PRODUCTO: {product['name']}
DESCRIPCIÓN: {product.get('description', 'N/A')}
PRECIO: ${product['price']:,.2f} MXN
CATEGORÍA: {product['category']}
ESPECIFICACIONES: {json.dumps(product.get('specs', {}), ensure_ascii=False)}
PUNTOS DESTACADOS: {', '.join(product.get('highlights', []))}
PLATAFORMA: {platform}
LÍMITE DE CARACTERES: {char_limits.get(platform, 500)}
REQUISITOS:
- Destaca los beneficios principales
- Incluye el precio
- Usa emojis relevantes
- Incluye CTA sutil (ej: "Contáctanos", "Más info en DM")
- Termina con 2-3 hashtags
- NO inventes especificaciones
Responde SOLO con el texto del post."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=400,
temperature=0.7
)
return response.choices[0].message.content.strip()
async def generate_service_post(
self,
service: Dict,
platform: str
) -> str:
"""Generar post para un servicio."""
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 1000
}
prompt = f"""Genera un post promocional para este servicio:
SERVICIO: {service['name']}
DESCRIPCIÓN: {service.get('description', 'N/A')}
CATEGORÍA: {service['category']}
SECTORES OBJETIVO: {', '.join(service.get('target_sectors', []))}
BENEFICIOS: {', '.join(service.get('benefits', []))}
CTA: {service.get('call_to_action', 'Contáctanos para más información')}
PLATAFORMA: {platform}
LÍMITE DE CARACTERES: {char_limits.get(platform, 500)}
REQUISITOS:
- Enfócate en el problema que resuelve
- Destaca 2-3 beneficios clave
- Usa emojis relevantes (✅, 🚀, 💡)
- Incluye el CTA
- Termina con 2-3 hashtags
- Tono consultivo, no vendedor
Responde SOLO con el texto del post."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=400,
temperature=0.7
)
return response.choices[0].message.content.strip()
async def generate_thread(
self,
topic: str,
num_posts: int = 5
) -> List[str]:
"""Generar un hilo educativo."""
prompt = f"""Genera un hilo educativo de {num_posts} posts sobre: {topic}
REQUISITOS:
- Post 1: Gancho que capture atención
- Posts 2-{num_posts-1}: Contenido educativo de valor
- Post {num_posts}: Conclusión con CTA
FORMATO:
- Cada post máximo 280 caracteres
- Numera cada post (1/, 2/, etc.)
- Usa emojis relevantes
- El último post incluye hashtags
Responde con cada post en una línea separada, sin explicaciones."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=1500,
temperature=0.7
)
content = response.choices[0].message.content.strip()
# Separar posts por líneas no vacías
posts = [p.strip() for p in content.split('\n') if p.strip()]
return posts
async def generate_response_suggestion(
self,
interaction_content: str,
interaction_type: str,
context: Optional[str] = None
) -> List[str]:
"""Generar sugerencias de respuesta para una interacción."""
prompt = f"""Un usuario escribió esto en redes sociales:
"{interaction_content}"
TIPO DE INTERACCIÓN: {interaction_type}
{f'CONTEXTO ADICIONAL: {context}' if context else ''}
Genera 3 opciones de respuesta diferentes:
1. Respuesta corta y amigable
2. Respuesta que invite a continuar la conversación
3. Respuesta que dirija a más información/contacto
REQUISITOS:
- Máximo 280 caracteres cada una
- Tono amigable y profesional
- Si es una queja, sé empático
- Si es una pregunta técnica, sé útil
Responde con las 3 opciones numeradas, una por línea."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=500,
temperature=0.8
)
content = response.choices[0].message.content.strip()
suggestions = [s.strip() for s in content.split('\n') if s.strip()]
# Limpiar numeración si existe
cleaned = []
for s in suggestions:
if s[0].isdigit() and (s[1] == '.' or s[1] == ')'):
s = s[2:].strip()
cleaned.append(s)
return cleaned[:3] # Máximo 3 sugerencias
async def adapt_content_for_platform(
self,
content: str,
target_platform: str
) -> str:
"""Adaptar contenido existente a una plataforma específica."""
char_limits = {
"x": 280,
"threads": 500,
"instagram": 2200,
"facebook": 1000
}
prompt = f"""Adapta este contenido para {target_platform}:
CONTENIDO ORIGINAL:
{content}
LÍMITE DE CARACTERES: {char_limits.get(target_platform, 500)}
REQUISITOS PARA {target_platform.upper()}:
{"- Muy conciso, directo al punto" if target_platform == "x" else ""}
{"- Puede ser más extenso, incluir más contexto" if target_platform == "instagram" else ""}
{"- Tono más casual y cercano" if target_platform == "threads" else ""}
{"- Puede incluir links, más profesional" if target_platform == "facebook" else ""}
- Mantén la esencia del mensaje
- Ajusta hashtags según la plataforma
Responde SOLO con el contenido adaptado."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self._get_system_prompt()},
{"role": "user", "content": prompt}
],
max_tokens=400,
temperature=0.6
)
return response.choices[0].message.content.strip()
# Instancia global
content_generator = ContentGenerator()

View File

@@ -0,0 +1,174 @@
"""
Servicio de generación de imágenes para posts.
"""
import os
from typing import Dict, Optional
from pathlib import Path
from html2image import Html2Image
from PIL import Image
from jinja2 import Environment, FileSystemLoader
from app.core.config import settings
class ImageGenerator:
"""Generador de imágenes usando plantillas HTML."""
def __init__(self):
self.templates_dir = Path("templates")
self.output_dir = Path("uploads/generated")
self.output_dir.mkdir(parents=True, exist_ok=True)
self.jinja_env = Environment(
loader=FileSystemLoader(self.templates_dir)
)
self.hti = Html2Image(
output_path=str(self.output_dir),
custom_flags=['--no-sandbox', '--disable-gpu']
)
def _render_template(self, template_name: str, variables: Dict) -> str:
"""Renderizar una plantilla HTML con variables."""
template = self.jinja_env.get_template(template_name)
return template.render(**variables)
async def generate_tip_card(
self,
title: str,
content: str,
category: str,
output_name: str
) -> str:
"""Generar imagen de tip tech."""
variables = {
"title": title,
"content": content,
"category": category,
"logo_url": f"{settings.BUSINESS_WEBSITE}/logo.png",
"website": settings.BUSINESS_WEBSITE,
"business_name": settings.BUSINESS_NAME
}
html_content = self._render_template("tip_card.html", variables)
# Generar imagen
output_file = f"{output_name}.png"
self.hti.screenshot(
html_str=html_content,
save_as=output_file,
size=(1080, 1080)
)
return str(self.output_dir / output_file)
async def generate_product_card(
self,
name: str,
price: float,
image_url: str,
highlights: list,
output_name: str
) -> str:
"""Generar imagen de producto."""
variables = {
"name": name,
"price": f"${price:,.2f} MXN",
"image_url": image_url,
"highlights": highlights[:3], # Máximo 3 highlights
"logo_url": f"{settings.BUSINESS_WEBSITE}/logo.png",
"website": settings.BUSINESS_WEBSITE,
"business_name": settings.BUSINESS_NAME
}
html_content = self._render_template("product_card.html", variables)
output_file = f"{output_name}.png"
self.hti.screenshot(
html_str=html_content,
save_as=output_file,
size=(1080, 1080)
)
return str(self.output_dir / output_file)
async def generate_service_card(
self,
name: str,
tagline: str,
benefits: list,
icon: str,
output_name: str
) -> str:
"""Generar imagen de servicio."""
variables = {
"name": name,
"tagline": tagline,
"benefits": benefits[:4], # Máximo 4 beneficios
"icon": icon,
"logo_url": f"{settings.BUSINESS_WEBSITE}/logo.png",
"website": settings.BUSINESS_WEBSITE,
"business_name": settings.BUSINESS_NAME
}
html_content = self._render_template("service_card.html", variables)
output_file = f"{output_name}.png"
self.hti.screenshot(
html_str=html_content,
save_as=output_file,
size=(1080, 1080)
)
return str(self.output_dir / output_file)
async def resize_for_platform(
self,
image_path: str,
platform: str
) -> str:
"""Redimensionar imagen para una plataforma específica."""
sizes = {
"x": (1200, 675), # 16:9
"threads": (1080, 1080), # 1:1
"instagram": (1080, 1080), # 1:1
"facebook": (1200, 630) # ~1.9:1
}
target_size = sizes.get(platform, (1080, 1080))
img = Image.open(image_path)
# Crear nueva imagen con el tamaño objetivo
new_img = Image.new('RGB', target_size, (26, 26, 46)) # Color de fondo de la marca
# Calcular posición para centrar
img_ratio = img.width / img.height
target_ratio = target_size[0] / target_size[1]
if img_ratio > target_ratio:
# Imagen más ancha
new_width = target_size[0]
new_height = int(new_width / img_ratio)
else:
# Imagen más alta
new_height = target_size[1]
new_width = int(new_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Centrar en la nueva imagen
x = (target_size[0] - new_width) // 2
y = (target_size[1] - new_height) // 2
new_img.paste(img_resized, (x, y))
# Guardar
output_path = image_path.replace('.png', f'_{platform}.png')
new_img.save(output_path, 'PNG', quality=95)
return output_path
# Instancia global
image_generator = ImageGenerator()