Estar presente hoy significa publicar video constantemente. Pero grabar, editar y publicar cada dia te roba horas o te obliga a pagar a un editor, una agencia o un community manager.
Mientras tanto, tu competencia publica todos los dias y se queda con la atencion de tu cliente. Necesitas estar presente sin que te consuma la vida.
La solucion
UN SISTEMA QUE TRABAJA POR TI
β Genera guiones con psicologia de retencion
β Crea voz natural en el acento de tu pais
β Monta el video con tus imagenes o las suyas
β AΓ±ade subtitulos y tu mensaje de marca
β Publica solo en tu canal cada 4 horas
A quien sirve
ΒΏPARA QUIEN ES?
πͺ Negocios que quieren mas clientes sin gastar en publicidad cara
π Emprendedores que venden un producto o servicio
π¨ Creadores y marcas que quieren presencia sin grabar
βοΈ Quien quiere estar en todas partes sin perder tiempo
Como funciona
EN 4 PASOS
01
Eliges tu tema y tu mensaje
Le dices de que va tu marca o producto y que quieres comunicar.
02
El sistema crea el contenido
Guion con psicologia de venta, voz de tu pais, imagenes y montaje.
03
El video se produce solo
Composicion profesional con tu material o el del sistema.
04
Se publica en tu canal
Tu presencia crece sola, cada dia, sin que toques nada.
ΒΏPor que TubeBot?
LO QUE TE DA
π¬
Composicion Profesional
Cada imagen se procesa con fondo y composicion cinematografica. Parece hecho por un editor.
ποΈ
Voz de Tu Pais
πͺπΈ π²π½ π¦π· π¨π΄ πΊπΈ π¬π§ π§π· Conecta con tu audiencia local.
πΈ
Tu Material o el Suyo
Sube tus imagenes y videos, o deja que el sistema los ponga. Tu marca, tu estilo.
π§
Psicologia de Venta
Los guiones usan gancho, retencion y llamada a la accion. No solo informan, mueven a actuar.
π¨
100% Personalizable
Color de subtitulos, frases, link en descripcion, tono. Todo a tu gusto sin tocar codigo.
π€
Totalmente Automatico
Una vez activado, publica solo cada 4 horas. Tu presencia crece mientras duermes.
π‘οΈ
GARANTIA DE 7 DIAS
Si el sistema no funciona como prometo, te devuelvo el 100%. Sin preguntas.
Planes
ELIGE TU PLAN
Pago unico. Sin mensualidades. Acceso de por vida.
1οΈβ£ Entra a tu URL β Si pide login pulsa "Sign in as guest" o "Opciones avanzadas" para entrar sin cuenta
2οΈβ£ Espera 2-3 minutos β La primera vez tarda en arrancar, es normal
3οΈβ£ VerΓ‘s los logs β Pantalla negra con texto dorado mostrando el bot en tiempo real
4οΈβ£ El bot trabaja solo β Genera guiΓ³n, voz, vΓdeo y lo sube a YouTube cada 4 horas
5οΈβ£ Sin tocar nada β Funciona 24/7 completamente solo
6οΈβ£ ΒΏProblemas? β EscrΓbeme y te ayudo en minutos
"""
# ββββββββββββββββββββββββββββββββββββββββββ
# RUTAS
# ββββββββββββββββββββββββββββββββββββββββββ
VISITAS_CACHE = {} # IP -> timestamp para no spamear avisos
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
try:
ip = request.client.host if request.client else "?"
import time as _t
ahora = _t.time()
# Avisar solo si esa IP no visito en los ultimos 10 min
if ip not in VISITAS_CACHE or ahora - VISITAS_CACHE[ip] > 600:
VISITAS_CACHE[ip] = ahora
ua = request.headers.get("user-agent","?")[:80]
# Filtrar bots HF y crawlers
ignorar = ["HuggingFace", "bot", "spider", "crawler", "python-requests", "curl"]
if any(b.lower() in ua.lower() for b in ignorar):
pass # No avisar
else:
referer = request.headers.get("referer", "directo/sin referer")
avisar_telegram(f"π€ Visita a landing\nπ IP: {ip}\nπ± {ua}\nπ Viene de: {referer}")
except: pass
return VENTAS_HTML
PLANES_CANALES = {"basico": 1, "oro": 5, "diamante": 9, "platinum": 20}
@app.get("/activar-canal", response_class=HTMLResponse)
async def activar_canal(token: str = "", plan: str = ""):
if plan and plan in PLANES_CANALES:
import secrets
nuevo_token = secrets.token_urlsafe(32)
TOKENS_VALIDOS[nuevo_token] = {"plan": plan}
avisar_telegram(f"π° POSIBLE VENTA\nπ¦ Plan: {plan.upper()}\nπ₯ Cliente llego a activar-canal tras pago Stripe")
return ACTIVAR_HTML.replace("PLAN_PLACEHOLDER", plan).replace("TOKEN_PLACEHOLDER", nuevo_token).replace("CANALES_PLACEHOLDER", str(PLANES_CANALES[plan]))
if token and token in TOKENS_VALIDOS:
plan_token = TOKENS_VALIDOS[token].get("plan", "basico") if isinstance(TOKENS_VALIDOS[token], dict) else "basico"
return ACTIVAR_HTML.replace("PLAN_PLACEHOLDER", plan_token).replace("TOKEN_PLACEHOLDER", token).replace("CANALES_PLACEHOLDER", str(PLANES_CANALES.get(plan_token, 1)))
return HTMLResponse("
Acceso no autorizado
Volver", status_code=403)
async def parsear_config_groq(texto):
"""Parsea instrucciones del cliente con Groq a JSON de configuracion"""
if not texto or not texto.strip():
return {}
try:
import httpx
prompt = f"""Analiza las preferencias del cliente y devuelve SOLO un JSON valido con estas keys (omite las que no se mencionen):
- voz_ia: true/false (si quiere voz IA, default true si no dice nada)
- voz_genero: "masculina" o "femenina" (default masculina)
- pais_voz: pais del acento de voz si lo menciona el cliente. Valores: "espaΓ±a", "mexico", "argentina", "colombia", "usa", "uk", "australia", "brasil", "francia". Si no menciona pais, dejar vacio
- idioma: "es"/"en"/"pt"/"fr" (default es)
- velocidad_voz: 0.8 a 1.5 (default 1.0)
- subir_musica: true/false (default false)
- subir_voz: true/false (default false)
- tono: "serio"/"divertido"/"motivacional"/"educativo" (default educativo)
- subtitulos: true/false (default true)
- estilo_subtitulos: "tiktok"/"clasico"/"minimal" (default tiktok)
- color_subtitulos: codigo hex como "#D4AF37" (default #D4AF37)
- posicion_subtitulos: "arriba"/"centro"/"abajo" (default abajo)
- frase_inicio: texto literal que el cliente quiere al inicio del video (string vacio si no menciona)
- frase_final: texto literal que el cliente quiere al final del video (string vacio si no menciona)
- canal_promocionar: URL del canal que quiere promocionar (string vacio si no hay)
- link_descripcion: URL/link a poner en descripcion del video (string vacio si no hay)
Preferencias del cliente: {texto}
Responde SOLO el JSON, sin explicaciones, sin markdown."""
async with httpx.AsyncClient(timeout=20) as cli:
r = await cli.post("https://api.groq.com/openai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ.get('GROQ_KEY','')}"},
json={"model":"llama-3.3-70b-versatile","messages":[{"role":"user","content":prompt}],"temperature":0.2}
)
txt = r.json()["choices"][0]["message"]["content"].strip()
# Limpiar markdown si lo trae
if "```" in txt:
txt = txt.split("```")[1].replace("json","",1).strip()
import json
return json.loads(txt)
except Exception as e:
print(f"Error parseando config: {e}", flush=True)
return {}
@app.post("/crear")
async def crear(request: Request):
data = await request.json()
email = data.get("email","").strip().lower()
canales_data = data.get("canales", [])
archivos_subidos = data.get("archivos", [])
if canales_data:
nicho = canales_data[0].get("nicho","tecnologia")
tipo = canales_data[0].get("tipo","largo")
config_libre = canales_data[0].get("config","").strip()
else:
nicho = data.get("nicho","tecnologia")
tipo = data.get("tipo","largo")
config_libre = ""
token = data.get("token","")
# Verificar token o sesion existente
plan = "basico"
if token and token in TOKENS_VALIDOS:
plan = TOKENS_VALIDOS[token].get("plan", "basico") if isinstance(TOKENS_VALIDOS[token], dict) else "basico"
elif email in CLIENTES and CLIENTES[email].get("plan"):
plan = CLIENTES[email]["plan"]
else:
return JSONResponse({"error": "Token invalido"}, status_code=403)
# Verificar limite del plan
limite = PLANES_CANALES.get(plan, 1)
canales_actuales = len(CLIENTES.get(email, {}).get("canales", []))
if canales_actuales >= limite:
return JSONResponse({"error": f"Has alcanzado el limite de tu plan {plan} ({limite} canales)"}, status_code=400)
# Parsear preferencias del cliente con Groq ANTES de crear
config_parsed = await parsear_config_groq(config_libre)
# Asignar proyecto disponible y guardarlo en config
if not isinstance(config_parsed, dict):
config_parsed = {}
idx_disponible = proyecto_disponible()
config_parsed["proyecto_idx"] = idx_disponible
# Crear el canal pasando la config
resultado = await crear_space(nicho, email, tipo, config_parsed, archivos_subidos)
if not resultado:
return JSONResponse({"error": "No se pudo crear el canal"}, status_code=500)
u = resultado["url"]
repo = resultado["repo"]
# Guardar en CLIENTES
if email not in CLIENTES:
CLIENTES[email] = {"canales": [], "plan": plan}
CLIENTES[email]["plan"] = plan
if "canales" not in CLIENTES[email]:
CLIENTES[email]["canales"] = []
CLIENTES[email]["canales"].append({"url": u, "nicho": nicho, "tipo": tipo, "repo": repo, "youtube": False, "config": config_parsed, "config_texto": config_libre})
guardar_clientes()
avisar_telegram(f"π¬ Canal creado\nπ§ {email}\nπΊ Plan: {plan}\nπ― Nicho: {nicho}\nπ {u}")
# Consumir token si era nuevo
if token in TOKENS_VALIDOS:
del TOKENS_VALIDOS[token]
urls = [c["url"] for c in CLIENTES[email]["canales"]]
try:
enviar_email_multi(email, urls)
except Exception as e:
print(f"Error email: {e}", flush=True)
return {"success": True, "url": u, "total": len(urls), "limite": limite}
@app.post("/editar-config")
async def editar_config(request: Request):
data = await request.json()
email = data.get("email","").strip().lower()
canal_idx = data.get("canal_idx", -1)
nuevo_texto = data.get("config_texto","").strip()
if email not in CLIENTES:
return JSONResponse({"error":"Cuenta no encontrada"}, status_code=404)
canales = CLIENTES[email].get("canales", [])
if canal_idx < 0 or canal_idx >= len(canales):
return JSONResponse({"error":"Canal no encontrado"}, status_code=404)
canal = canales[canal_idx]
repo = canal.get("repo","")
nicho = canal.get("nicho","tecnologia")
tipo = canal.get("tipo","largo")
token_hf = token_de_repo(repo)
if not token_hf:
return JSONResponse({"error":"No se pudo acceder al canal"}, status_code=500)
# Re-parsear la nueva config con Groq
config_parsed = await parsear_config_groq(nuevo_texto)
if not isinstance(config_parsed, dict):
config_parsed = {}
# Conservar el proyecto google asignado
if "proyecto_idx" in canal.get("config", {}):
config_parsed["proyecto_idx"] = canal["config"]["proyecto_idx"]
# Regenerar el main.py del canal con la nueva config
import base64, json as _json
def b64(s): return base64.b64encode(s.encode("utf-8")).decode("utf-8")
with open("cliente_template.py","r") as _f:
main_code = _f.read().replace("NICHO_PLACEHOLDER", nicho).replace("EMAIL_PLACEHOLDER", email).replace("REPO_PLACEHOLDER", repo).replace("GROQ_PLACEHOLDER", os.environ.get("GROQ_KEY","")).replace("GROQ_PLACEHOLDER_2", os.environ.get("GROQ_KEY_2","")).replace("GROQ_PLACEHOLDER_3", os.environ.get("GROQ_KEY_3","")).replace("PEXELS_PLACEHOLDER", os.environ.get("PEXELS_KEY","")).replace("TIPO_PLACEHOLDER", tipo).replace("CONFIG_PLACEHOLDER", _json.dumps(config_parsed))
commit_payload = {
"summary": "Editar config",
"files": [{"path":"main.py","content":b64(main_code),"encoding":"base64"}]
}
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
f"https://huggingface.co/api/spaces/{repo}/commit/main",
headers={"Authorization": f"Bearer {token_hf}", "Content-Type":"application/json"},
content=_json.dumps(commit_payload).encode()
)
if r.status_code not in [200,201]:
return JSONResponse({"error":"No se pudo aplicar"}, status_code=500)
# Guardar la nueva config en CLIENTES
CLIENTES[email]["canales"][canal_idx]["config"] = config_parsed
CLIENTES[email]["canales"][canal_idx]["config_texto"] = nuevo_texto
guardar_clientes()
return {"success": True}
@app.get("/mi-cuenta", response_class=HTMLResponse)
async def mi_cuenta(email: str = ""):
email = email.strip().lower()
if not email or email not in CLIENTES:
return HTMLResponse("
Cuenta no encontrada
Verifica el email correcto.
", status_code=404)
cliente = CLIENTES[email]
plan = cliente.get("plan", "basico")
limite = PLANES_CANALES.get(plan, 1)
canales = cliente.get("canales", [])
restantes = limite - len(canales)
lista_html = ""
for i, ch in enumerate(canales, 1):
ch_url = ch.get("url","#")
yt_conectado = ch.get("youtube", False)
if yt_conectado:
yt_btn = '
Guarda este enlace para gestionar tus canales: {url_cuenta}
Si tienes dudas responde este email.
"""
requests.post("https://api.brevo.com/v3/smtp/email",
headers={"api-key": os.environ.get("BREVO_KEY",""), "Content-Type": "application/json", "accept": "application/json"},
json={
"sender": {"name": "TubeBot", "email": "dayron100813@gmail.com"},
"to": [{"email": email}],
"subject": f"Tus {len(urls)} canales estan listos",
"htmlContent": html
}
)
except Exception as e:
print(f"Error email: {e}", flush=True)
@app.get("/check-space")
async def check_space(url: str = ""):
try:
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
r = await client.get(url)
if r.status_code == 200 and "gradio" in r.text.lower():
return {"ready": True}
except:
pass
return {"ready": False}
@app.get("/check-space")
async def check_space(url: str = ""):
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(url)
return {"ready": r.status_code == 200}
except:
return {"ready": False}
@app.get("/esperando", response_class=HTMLResponse)
async def esperando(url: str = "", email: str = ""):
return f"""
TubeBot β Preparando tu canal...
π€ CREANDO TU CANAL
Tu sistema estΓ‘ siendo construido en nuestros servidores.
Este proceso tarda entre 3-5 minutos la primera vez.
β‘ Instalando dependencias...
β οΈ Aviso normal de Google: VerΓ‘s "App no verificada". Es seguro. Pulsa "ConfiguraciΓ³n avanzada" y luego "Ir a TubeBot (no seguro)". Es porque la app aΓΊn estΓ‘ en revisiΓ³n por Google.
β³ Reiniciando tu sistema con YouTube conectado... Espera 2 minutos.
π‘ Guarda esta URL en favoritos: {url_cliente}
ΒΏProblemas? WhatsApp: +34656691085
""")
# restart
@app.post("/reportar-uso")
async def reportar_uso(request: Request):
"""Endpoint que cada cliente Space llama cuando sube un video"""
try:
data = await request.json()
idx = data.get("proyecto_idx", -1)
email = data.get("email", "?")
titulo = data.get("titulo", "?")
if 0 <= idx < len(GOOGLE_PROYECTOS):
incrementar_uso(idx)
avisar_telegram(f"πΊ Video subido a YouTube\nπ§ {email}\nπ¬ {titulo[:60]}\nπ Proyecto {idx+1}: {USOS_PROYECTO[idx]['count']} videos hoy")
return {"ok": True, "usos_hoy": USOS_PROYECTO[idx]["count"]}
except Exception as e:
print(f"Error reportar uso: {e}", flush=True)
return {"ok": False}
@app.get("/estado-proyectos")
async def estado_proyectos():
"""Ver estado de los proyectos Google"""
estado = []
for i in range(len(GOOGLE_PROYECTOS)):
reset_si_nuevo_dia(i)
estado.append({"proyecto": i+1, "usos_hoy": USOS_PROYECTO[i]["count"]})
return estado