IAweb / main.py
Tu Nombre
Fix: añadir referer a avisos de visitas
fb0afae
Raw
History Blame Contribute Delete
75.2 kB
import os, re, time, asyncio, threading, httpx
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
@app.on_event("startup")
async def iniciar_pings():
import asyncio, httpx
async def ping_loop():
while True:
await asyncio.sleep(21600) # 6 horas
try:
async with httpx.AsyncClient(timeout=15) as client:
for email, datos in list(CLIENTES.items()):
url = datos.get("url", "")
if url:
try:
await client.get(url)
print(f"Ping OK: {url}", flush=True)
except Exception as e:
print(f"Ping fail {url}: {e}", flush=True)
except Exception as ex:
print(f"Error ping loop: {ex}", flush=True)
asyncio.create_task(ping_loop())
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
HF_USER = os.environ.get("HF_USER", "Cristobal299")
HF_TOKEN = os.environ.get("HF_TOKEN", "")
GROQ_KEY = os.environ.get("GROQ_KEY", "")
PEXELS_KEY = os.environ.get("PEXELS_KEY", "")
STRIPE_KEY = os.environ.get("STRIPE_KEY", "")
STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "")
TOKENS_VALIDOS = {}
@app.post("/stripe-webhook")
async def stripe_webhook(request: Request):
import hmac, hashlib, secrets
payload = await request.body()
sig = request.headers.get("stripe-signature","")
try:
elements = {k:v for k,v in [x.split("=",1) for x in sig.split(",")]}
ts = elements.get("t","")
expected = hmac.new(STRIPE_WEBHOOK_SECRET.encode(), f"{ts}.{payload.decode()}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, elements.get("v1","")):
return JSONResponse({"error":"firma invalida"}, status_code=400)
except:
return JSONResponse({"error":"error verificacion"}, status_code=400)
import json
data = json.loads(payload)
if data.get("type") == "checkout.session.completed":
email = data["data"]["object"].get("customer_email","")
token = secrets.token_urlsafe(32)
TOKENS_VALIDOS[token] = email
return {"ok": True}
RESEND_KEY = os.environ.get("RESEND_KEY", "")
def enviar_email(email, nicho, url):
try:
import requests
html = f"""
<div style="background:#0a0a0a;color:#fff;padding:40px;font-family:sans-serif;max-width:600px;margin:0 auto;">
<h1 style="color:#ff2d2d;">🤖 TubeBot activado</h1>
<p>Tu canal de <strong>{nicho}</strong> ya está funcionando.</p>
<p style="margin:20px 0;">
<a href="{url}" style="background:#ff2d2d;color:#fff;padding:14px 28px;border-radius:6px;text-decoration:none;font-weight:bold;">
Ver mi sistema en tiempo real
</a>
</p>
<p style="color:#666;font-size:.85rem;">Si tienes dudas responde este email.</p>
</div>"""
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": "Tu canal de YouTube ya esta listo",
"htmlContent": html
}
)
except Exception as e:
print(f"Error email: {e}")
CLIENTES = {}
def _enviar_aviso_real(asunto, mensaje):
try:
import requests as _r
html = mensaje.replace("<b>", "<strong>").replace("</b>", "</strong>").replace("\n", "<br>")
_r.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 Avisos", "email": "dayron100813@gmail.com"},
"to": [{"email": "Sicariodedios69@gmail.com"}],
"subject": asunto,
"htmlContent": f"<div style='font-family:sans-serif;background:#0a0a0a;color:#fff;padding:20px'>{html}</div>"
},
timeout=30
)
except Exception as e:
print(f"Error aviso: {e}", flush=True)
def avisar_telegram(mensaje):
"""Manda aviso por Gmail (Brevo) en thread aparte"""
import threading
asunto_corto = mensaje.split("\n")[0].replace("<b>", "").replace("</b>", "").strip()
threading.Thread(target=_enviar_aviso_real, args=(asunto_corto[:100], mensaje), daemon=True).start()
def cargar_clientes():
global CLIENTES
try:
import json
r = __import__("requests").get(
f"https://huggingface.co/api/repos/Cristobal299/IAweb/raw/main/clientes.json",
headers={"Authorization": f"Bearer {HF_TOKEN}"}
)
if r.status_code == 200:
CLIENTES = r.json()
except:
pass
def guardar_clientes():
try:
import json
data = json.dumps(CLIENTES).encode()
__import__("requests").put(
f"https://huggingface.co/api/repos/Cristobal299/IAweb/upload/main/clientes.json",
headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/octet-stream"},
data=data
)
except Exception as e:
print(f"Error guardando clientes: {e}")
cargar_clientes()
# ══════════════════════════════════════════
# CREAR SPACE EN HUGGING FACE
# ══════════════════════════════════════════
def token_de_repo(repo):
"""Del repo (ej Maria288/tb-xyz) saca el usuario y devuelve su token HF"""
usuario = repo.split("/")[0] if "/" in repo else ""
mapa = {
os.environ.get("HF_USER_5","Alejandro"): os.environ.get("HF_TOKEN_5",""),
os.environ.get("HF_USER_4","Maria288"): os.environ.get("HF_TOKEN_4",""),
os.environ.get("HF_USER_2","Cristobal2999"): os.environ.get("HF_TOKEN_2",""),
os.environ.get("HF_USER_3","Cristobal288"): os.environ.get("HF_TOKEN_3",""),
}
return mapa.get(usuario, "")
async def crear_space(nicho: str, email: str, tipo: str = "largo", config_parsed: dict = None, archivos_subidos: list = None) -> str:
space_id = f"tb-{re.sub(r'[^a-z0-9]','', nicho.lower()[:12])}-{int(time.time())%9999}"
cuentas = [
(os.environ.get("HF_TOKEN_5",""), os.environ.get("HF_USER_5","Alejandro")),
(os.environ.get("HF_TOKEN_4",""), os.environ.get("HF_USER_4","Maria288")),
(os.environ.get("HF_TOKEN_2",""), os.environ.get("HF_USER_2","Cristobal2999")),
(os.environ.get("HF_TOKEN_3",""), os.environ.get("HF_USER_3","Cristobal288")),
]
cuentas = [(t,u) for t,u in cuentas if t]
import random
token_actual, hf_user_actual = random.choice(cuentas)
print(f"DEBUG user={hf_user_actual} token={token_actual[:8]}", flush=True)
headers = {"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"}
with open("cliente_template.py", "r") as _f:
main_code = _f.read().replace("NICHO_PLACEHOLDER", nicho).replace("EMAIL_PLACEHOLDER", email).replace("REPO_PLACEHOLDER", f"{hf_user_actual}/{space_id}").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", __import__("json").dumps(config_parsed or {}))
req = "gradio\ngroq\nedge-tts\nmoviepy==1.0.3\nrequests\nhttpx\ngoogle-api-python-client\ngoogle-auth\ngoogle-auth-oauthlib\nimageio\nimageio-ffmpeg\ndecorator\ntqdm\nPillow\nnumpy\nopencv-python-headless"
async with httpx.AsyncClient(timeout=30) as client:
# Crear repo
r = await client.post("https://huggingface.co/api/repos/create",
headers=headers,
json={"name": space_id, "type": "space", "sdk": "docker", "private": False})
repo_name = r.json().get("name", f"{hf_user_actual}/{space_id}")
if r.status_code not in [200, 201]:
print(f"Error repo: {r.text}")
return None
repo_name = r.json().get("name", f"{hf_user_actual}/{space_id}")
print(f"Repo OK: {repo_name}", flush=True)
# Subir archivos via commit API
import base64, json as _json
def b64(s): return base64.b64encode(s.encode("utf-8")).decode("utf-8")
readme = """---
title: TubeBot
emoji: 🤖
colorFrom: red
colorTo: gray
sdk: docker
pinned: false
---"""
dockerfile = """FROM python:3.10-slim
RUN apt-get update && apt-get install -y imagemagick ffmpeg && rm -rf /var/lib/apt/lists/*
RUN sed -i "s/rights=\"none\"/rights=\"read|write\"/g" /etc/ImageMagick-7/policy.xml || true
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY main.py .
COPY media_* ./
EXPOSE 7860
CMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"7860\"]"""
commit_payload = {
"summary": "TubeBot init",
"files": [
{"path": "main.py", "content": b64(main_code), "encoding": "base64"},
{"path": "requirements.txt", "content": b64(req), "encoding": "base64"},
{"path": "README.md", "content": b64(readme), "encoding": "base64"},
{"path": "Dockerfile", "content": b64(dockerfile), "encoding": "base64"}
]
}
r2 = await client.post(
f"https://huggingface.co/api/spaces/{repo_name}/commit/main",
headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"},
content=_json.dumps(commit_payload).encode()
)
print(f"Commit: {r2.status_code} {r2.text[:200]}")
# Subir archivos del cliente uno a uno (evita problema de binarios)
if archivos_subidos:
for i, arc in enumerate(archivos_subidos[:20]):
try:
nombre = arc.get("nombre", f"media_{i}.jpg").replace("/","_").replace("\\","_")
contenido_b64 = arc.get("contenido", "")
if not contenido_b64: continue
binario = base64.b64decode(contenido_b64)
commit_arc = {
"summary": f"upload {nombre}",
"files": [
{"path": f"media_{nombre}", "content": contenido_b64, "encoding": "base64"}
]
}
# 3 intentos con espera
for intento in range(3):
r_up = await client.post(
f"https://huggingface.co/api/spaces/{repo_name}/commit/main",
headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"},
content=_json.dumps(commit_arc).encode()
)
if r_up.status_code in [200, 201]:
break
await asyncio.sleep(2)
print(f"Upload {nombre}: {r_up.status_code}", flush=True)
except Exception as e:
print(f"Error upload {arc.get('nombre','?')}: {e}", flush=True)
# Agregar secretos al Space del cliente
secretos = {
"GROQ_KEY": os.environ.get("GROQ_KEY",""),
"PEXELS_KEY": os.environ.get("PEXELS_KEY",""),
"GOOGLE_CLIENT_ID": os.environ.get("GOOGLE_CLIENT_ID",""),
"GOOGLE_CLIENT_SECRET": os.environ.get("GOOGLE_CLIENT_SECRET",""),
"CLIENT_EMAIL": email,
}
for key, val in secretos.items():
await client.post(
f"https://huggingface.co/api/spaces/{hf_user_actual}/{space_id}/secrets",
headers={"Authorization": f"Bearer {token_actual}", "Content-Type": "application/json"},
json={"key": key, "value": val}
)
url = f"https://{hf_user_actual.lower()}-{space_id.lower()}.hf.space"
return {"url": url, "repo": repo_name}
# ══════════════════════════════════════════
# PÁGINA PRINCIPAL — VENTAS
# ══════════════════════════════════════════
VENTAS_HTML = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TubeBot — Canal de YouTube Automático</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Manrope:wght@300;400;700;800&display=swap" rel="stylesheet">
<style>
:root{--r:#ff2d2d;--bg:#080808;--card:#111;--b:#1e1e1e;--t:#f0f0f0;--m:#666;}
*{box-sizing:border-box;margin:0;padding:0;}
html{scroll-behavior:smooth;}
body{background:var(--bg);color:var(--t);font-family:'Manrope',sans-serif;overflow-x:hidden;}
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:40px 20px;background:radial-gradient(ellipse 80% 50% at 50% 0%,#2a0000,transparent 70%);}
.badge{display:inline-block;background:#1a0000;border:1px solid var(--r);color:var(--r);font-size:.7rem;letter-spacing:3px;text-transform:uppercase;padding:6px 16px;border-radius:100px;margin-bottom:28px;animation:fd .6s ease both;}
h1{font-family:'Bebas Neue',sans-serif;font-size:clamp(3.5rem,12vw,8rem);line-height:.9;animation:fd .6s .1s ease both;opacity:0;}
h1 span{color:var(--r);}
.sub{font-size:clamp(1rem,2.5vw,1.3rem);color:#aaa;font-weight:300;max-width:560px;margin:24px auto 0;line-height:1.6;animation:fd .6s .2s ease both;opacity:0;}
.hero-cta{margin-top:40px;animation:fd .6s .3s ease both;opacity:0;}
.btn{display:inline-block;background:var(--r);color:#fff;font-family:'Manrope',sans-serif;font-weight:800;font-size:1.1rem;padding:18px 44px;border-radius:6px;text-decoration:none;cursor:pointer;border:none;transition:all .2s;}
.btn:hover{background:#cc0000;transform:translateY(-2px);box-shadow:0 8px 30px rgba(255,45,45,.4);}
.price-note{display:block;margin-top:14px;color:var(--m);font-size:.85rem;}
.price-note strong{color:var(--t);}
.stats{display:flex;justify-content:center;gap:clamp(20px,5vw,60px);flex-wrap:wrap;padding:60px 20px;border-top:1px solid var(--b);border-bottom:1px solid var(--b);background:#0d0d0d;}
.stat-n{font-family:'Bebas Neue',sans-serif;font-size:clamp(2.5rem,6vw,4rem);color:var(--r);line-height:1;}
.stat-l{font-size:.8rem;color:var(--m);letter-spacing:2px;text-transform:uppercase;margin-top:4px;}
.sec{padding:80px 20px;max-width:900px;margin:0 auto;}
.sec-label{font-size:.72rem;letter-spacing:4px;text-transform:uppercase;color:var(--r);margin-bottom:16px;font-weight:700;}
.sec h2{font-family:'Bebas Neue',sans-serif;font-size:clamp(2.5rem,6vw,4.5rem);line-height:1;margin-bottom:48px;}
.steps{display:flex;flex-direction:column;gap:16px;}
.step{display:flex;align-items:flex-start;gap:20px;background:var(--card);border:1px solid var(--b);border-radius:12px;padding:24px;}
.step-n{font-family:'Bebas Neue',sans-serif;font-size:2.5rem;color:var(--r);line-height:1;min-width:48px;}
.step-title{font-weight:700;margin-bottom:6px;}
.step-desc{color:#888;font-size:.9rem;line-height:1.6;}
.feats{padding:80px 20px;background:#0d0d0d;border-top:1px solid var(--b);border-bottom:1px solid var(--b);}
.feats-inner{max-width:900px;margin:0 auto;}
.feat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px;margin-top:48px;}
.feat{background:var(--bg);border:1px solid var(--b);border-radius:12px;padding:24px;}
.feat-icon{font-size:1.8rem;margin-bottom:12px;}
.feat-title{font-weight:700;margin-bottom:6px;}
.feat-desc{color:#777;font-size:.88rem;line-height:1.5;}
.price-sec{padding:80px 20px;max-width:480px;margin:0 auto;text-align:center;}
.price-card{background:var(--card);border:2px solid var(--r);border-radius:16px;padding:40px 32px;margin-top:48px;position:relative;overflow:hidden;}
.price-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--r),#ff6b6b);}
.price-amount{font-family:'Bebas Neue',sans-serif;font-size:5rem;color:var(--r);line-height:1;}
.price-period{color:var(--m);font-size:.9rem;margin-bottom:28px;}
.price-feats{list-style:none;text-align:left;margin-bottom:32px;}
.price-feats li{padding:10px 0;border-bottom:1px solid var(--b);font-size:.9rem;display:flex;align-items:center;gap:10px;}
.price-feats li:last-child{border-bottom:none;}
.chk{color:var(--r);}
.faq-sec{padding:80px 20px;max-width:700px;margin:0 auto;}
.faq-item{border-bottom:1px solid var(--b);padding:20px 0;cursor:pointer;}
.faq-q{font-weight:700;display:flex;justify-content:space-between;font-size:.95rem;}
.faq-a{color:#888;font-size:.88rem;line-height:1.6;margin-top:12px;display:none;}
.faq-item.open .faq-a{display:block;}
.faq-arr{transition:transform .2s;color:var(--r);}
.faq-item.open .faq-arr{transform:rotate(180deg);}
.fcta{padding:100px 20px;text-align:center;background:radial-gradient(ellipse 80% 60% at 50% 100%,#2a0000,transparent 70%);}
.fcta h2{font-family:'Bebas Neue',sans-serif;font-size:clamp(2.5rem,7vw,5rem);line-height:1;margin-bottom:24px;}
.fcta h2 span{color:var(--r);}
footer{text-align:center;padding:24px;color:var(--m);font-size:.8rem;border-top:1px solid var(--b);}
@keyframes fd{from{opacity:0;transform:translateY(-20px);}to{opacity:1;transform:translateY(0);}}
</style>
</head>
<body>
<section class="hero">
<div class="badge">📢 Publicidad Automatica con IA</div>
<h1>TU MARCA<br><span>EN TODAS PARTES</span></h1>
<p class="sub">Genera contenido en video para tu negocio, marca o proyecto <strong style="color:#fff">sin grabar, sin editar y sin contratar a nadie.</strong><br><br>El sistema crea y publica videos solo. Tu presencia crece mientras tu trabajas en lo que importa.</p>
<div class="hero-cta">
<a href="#planes" class="btn">🚀 QUIERO MI SISTEMA</a>
<span class="price-note">Desde <strong>$49</strong> · Pago unico</span>
</div>
</section>
<div class="sec">
<div class="sec-label">El problema</div>
<h2>CREAR CONTENIDO<br><span style="color:#ff2d2d">CUESTA CARO</span></h2>
<p style="font-size:1.1rem;color:#aaa;line-height:1.8;margin-bottom:20px;">Estar presente hoy significa publicar video constantemente. Pero grabar, editar y publicar cada dia <strong style="color:#fff">te roba horas</strong> o te obliga a pagar a un editor, una agencia o un community manager.</p>
<p style="font-size:1.1rem;color:#aaa;line-height:1.8;">Mientras tanto, tu competencia publica todos los dias y se queda con la atencion de tu cliente. <strong style="color:#D4AF37">Necesitas estar presente sin que te consuma la vida.</strong></p>
</div>
<div class="sec">
<div class="sec-label">La solucion</div>
<h2>UN SISTEMA QUE<br>TRABAJA POR TI</h2>
<div style="background:#111;border-radius:15px;padding:30px;margin-top:30px">
<ul style="list-style:none;padding:0">
<li style="padding:14px 0;font-size:1.1rem;color:#fff;border-bottom:1px solid #222">✅ Genera guiones con psicologia de retencion</li>
<li style="padding:14px 0;font-size:1.1rem;color:#fff;border-bottom:1px solid #222">✅ Crea voz natural en el acento de tu pais</li>
<li style="padding:14px 0;font-size:1.1rem;color:#fff;border-bottom:1px solid #222">✅ Monta el video con tus imagenes o las suyas</li>
<li style="padding:14px 0;font-size:1.1rem;color:#fff;border-bottom:1px solid #222">✅ Añade subtitulos y tu mensaje de marca</li>
<li style="padding:14px 0;font-size:1.1rem;color:#fff">✅ Publica solo en tu canal cada 4 horas</li>
</ul>
</div>
</div>
<div class="feats">
<div class="feats-inner">
<div class="sec-label">A quien sirve</div>
<h2>¿PARA QUIEN ES?</h2>
<div style="background:#0d0d0d;border-radius:15px;padding:25px;margin-top:20px;border:1px solid #1e1e1e">
<ul style="list-style:none;padding:0">
<li style="padding:12px 0;color:#fff;font-size:1.05rem;border-bottom:1px solid #222">🏪 Negocios que quieren mas clientes sin gastar en publicidad cara</li>
<li style="padding:12px 0;color:#fff;font-size:1.05rem;border-bottom:1px solid #222">🚀 Emprendedores que venden un producto o servicio</li>
<li style="padding:12px 0;color:#fff;font-size:1.05rem;border-bottom:1px solid #222">🎨 Creadores y marcas que quieren presencia sin grabar</li>
<li style="padding:12px 0;color:#fff;font-size:1.05rem">✍️ Quien quiere estar en todas partes sin perder tiempo</li>
</ul>
</div>
</div>
</div>
<div class="sec">
<div class="sec-label">Como funciona</div>
<h2>EN 4 PASOS</h2>
<div class="steps">
<div class="step"><div class="step-n">01</div><div><div class="step-title">Eliges tu tema y tu mensaje</div><div class="step-desc">Le dices de que va tu marca o producto y que quieres comunicar.</div></div></div>
<div class="step"><div class="step-n">02</div><div><div class="step-title">El sistema crea el contenido</div><div class="step-desc">Guion con psicologia de venta, voz de tu pais, imagenes y montaje.</div></div></div>
<div class="step"><div class="step-n">03</div><div><div class="step-title">El video se produce solo</div><div class="step-desc">Composicion profesional con tu material o el del sistema.</div></div></div>
<div class="step"><div class="step-n">04</div><div><div class="step-title">Se publica en tu canal</div><div class="step-desc">Tu presencia crece sola, cada dia, sin que toques nada.</div></div></div>
</div>
</div>
<div class="feats">
<div class="feats-inner">
<div class="sec-label">¿Por que TubeBot?</div>
<h2>LO QUE TE DA</h2>
<div class="feat-grid">
<div class="feat"><div class="feat-icon">🎬</div><div class="feat-title">Composicion Profesional</div><div class="feat-desc">Cada imagen se procesa con fondo y composicion cinematografica. Parece hecho por un editor.</div></div>
<div class="feat"><div class="feat-icon">🎙️</div><div class="feat-title">Voz de Tu Pais</div><div class="feat-desc">🇪🇸 🇲🇽 🇦🇷 🇨🇴 🇺🇸 🇬🇧 🇧🇷 Conecta con tu audiencia local.</div></div>
<div class="feat"><div class="feat-icon">📸</div><div class="feat-title">Tu Material o el Suyo</div><div class="feat-desc">Sube tus imagenes y videos, o deja que el sistema los ponga. Tu marca, tu estilo.</div></div>
<div class="feat"><div class="feat-icon">🧠</div><div class="feat-title">Psicologia de Venta</div><div class="feat-desc">Los guiones usan gancho, retencion y llamada a la accion. No solo informan, mueven a actuar.</div></div>
<div class="feat"><div class="feat-icon">🎨</div><div class="feat-title">100% Personalizable</div><div class="feat-desc">Color de subtitulos, frases, link en descripcion, tono. Todo a tu gusto sin tocar codigo.</div></div>
<div class="feat"><div class="feat-icon">🤖</div><div class="feat-title">Totalmente Automatico</div><div class="feat-desc">Una vez activado, publica solo cada 4 horas. Tu presencia crece mientras duermes.</div></div>
</div>
</div>
</div>
<div style="background:#0a2a0a;border-top:2px solid #4caf50;border-bottom:2px solid #4caf50;padding:50px 20px;text-align:center;">
<div style="font-size:3rem;margin-bottom:10px">🛡️</div>
<h2 style="font-family:Bebas Neue,sans-serif;color:#4caf50;font-size:clamp(2rem,5vw,3.5rem);line-height:1;">GARANTIA DE 7 DIAS</h2>
<p style="color:#aaa;font-size:1.1rem;margin-top:16px;max-width:500px;margin-left:auto;margin-right:auto;">Si el sistema no funciona como prometo, te devuelvo el 100%. Sin preguntas.</p>
</div>
<div class="price-sec" id="planes">
<div class="sec-label" style="text-align:center;">Planes</div>
<h2 style="font-family:Bebas Neue,sans-serif;font-size:clamp(2.5rem,6vw,4.5rem);line-height:1;text-align:center;margin-bottom:20px;">ELIGE TU PLAN</h2>
<p style="text-align:center;color:#aaa;margin-bottom:10px;">Pago unico. Sin mensualidades. Acceso de por vida.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-top:30px;">
<div style="background:#111;border:1px solid #333;border-radius:14px;padding:28px 20px;display:flex;flex-direction:column">
<div style="font-weight:700;font-size:1.2rem;margin-bottom:6px">Basico</div>
<div style="font-family:Bebas Neue,sans-serif;font-size:3rem;color:#fff;line-height:1">$49</div>
<div style="color:#666;font-size:.8rem;margin-bottom:18px">Pago unico</div>
<ul style="list-style:none;padding:0;margin-bottom:20px;flex:1">
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ <b>1 canal</b> automatico</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ 1 tema a elegir</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Videos cada 4 horas</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Soporte por email</li>
</ul>
<a href="https://buy.stripe.com/00w7sL8J84QQ2ZdeGUdEs02" style="background:#333;color:#fff;padding:14px;border-radius:8px;text-decoration:none;text-align:center;font-weight:700;display:block">ELEGIR BASICO</a>
</div>
<div style="background:#111;border:2px solid #D4AF37;border-radius:14px;padding:28px 20px;display:flex;flex-direction:column;position:relative">
<div style="position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#D4AF37;color:#000;font-size:.7rem;font-weight:900;padding:4px 14px;border-radius:50px">POPULAR</div>
<div style="font-weight:700;font-size:1.2rem;margin-bottom:6px">🥇 Oro</div>
<div style="font-family:Bebas Neue,sans-serif;font-size:3rem;color:#D4AF37;line-height:1">$147</div>
<div style="color:#666;font-size:.8rem;margin-bottom:18px">Pago unico</div>
<ul style="list-style:none;padding:0;margin-bottom:20px;flex:1">
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ <b>5 canales</b> automaticos</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ 5 temas diferentes</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Videos cada 4 horas</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Soporte WhatsApp prioritario</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Cambio de tema gratis</li>
</ul>
<a href="https://buy.stripe.com/00w00j9Ncdnm7ftcyMdEs03" style="background:#D4AF37;color:#000;padding:14px;border-radius:8px;text-decoration:none;text-align:center;font-weight:900;display:block">ELEGIR ORO</a>
</div>
<div style="background:#111;border:1px solid #00bfff;border-radius:14px;padding:28px 20px;display:flex;flex-direction:column">
<div style="font-weight:700;font-size:1.2rem;margin-bottom:6px">💎 Diamante</div>
<div style="font-family:Bebas Neue,sans-serif;font-size:3rem;color:#00bfff;line-height:1">$297</div>
<div style="color:#666;font-size:.8rem;margin-bottom:18px">Pago unico</div>
<ul style="list-style:none;padding:0;margin-bottom:20px;flex:1">
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ <b>9 canales</b> automaticos</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Todos los temas</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Videos cada 4 horas</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Soporte VIP 24/7</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Temas premium exclusivos</li>
</ul>
<a href="https://buy.stripe.com/fZucN5e3s2II43h2YcdEs04" style="background:#00bfff;color:#000;padding:14px;border-radius:8px;text-decoration:none;text-align:center;font-weight:900;display:block">ELEGIR DIAMANTE</a>
</div>
<div style="background:#111;border:1px solid #b300ff;border-radius:14px;padding:28px 20px;display:flex;flex-direction:column">
<div style="font-weight:700;font-size:1.2rem;margin-bottom:6px">🏆 Platinum</div>
<div style="font-family:Bebas Neue,sans-serif;font-size:3rem;color:#b300ff;line-height:1">$497</div>
<div style="color:#666;font-size:.8rem;margin-bottom:18px">Pago unico</div>
<ul style="list-style:none;padding:0;margin-bottom:20px;flex:1">
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ <b>20 canales</b> automaticos</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Todos los temas + futuros</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Videos cada 4 horas</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ WhatsApp directo conmigo</li>
<li style="padding:8px 0;color:#fff;font-size:.95rem">✅ Funciones nuevas antes que nadie</li>
</ul>
<a href="https://buy.stripe.com/5kQ28r1gG1EEdDRgP2dEs06" style="background:#b300ff;color:#fff;padding:14px;border-radius:8px;text-decoration:none;text-align:center;font-weight:900;display:block">ELEGIR PLATINUM</a>
</div>
</div>
</div>
<div class="faq-sec">
<div class="sec-label" style="text-align:center;">FAQ</div>
<h2 style="font-family:Bebas Neue,sans-serif;font-size:clamp(2.5rem,6vw,4.5rem);line-height:1;text-align:center;margin-bottom:32px;">PREGUNTAS FRECUENTES</h2>
<div class="faq-item" onclick="this.classList.toggle('open')"><div class="faq-q">¿Necesito mostrar mi cara? <span class="faq-arr">▼</span></div><div class="faq-a">No. El sistema crea todo el contenido sin que aparezcas.</div></div>
<div class="faq-item" onclick="this.classList.toggle('open')"><div class="faq-q">¿Puedo usar mis propias imagenes y mi marca? <span class="faq-arr">▼</span></div><div class="faq-a">Si. Puedes subir tu material o dejar que el sistema lo ponga. Tu mensaje y tu identidad.</div></div>
<div class="faq-item" onclick="this.classList.toggle('open')"><div class="faq-q">¿Necesito experiencia tecnica? <span class="faq-arr">▼</span></div><div class="faq-a">No. Eliges tu tema y el sistema hace el resto solo.</div></div>
<div class="faq-item" onclick="this.classList.toggle('open')"><div class="faq-q">¿Sirve para promocionar mi negocio? <span class="faq-arr">▼</span></div><div class="faq-a">Si. Esa es justo su fuerza: presencia y publicidad automatica para tu marca o producto.</div></div>
<div class="faq-item" onclick="this.classList.toggle('open')"><div class="faq-q">¿Hay pagos mensuales? <span class="faq-arr">▼</span></div><div class="faq-a">No. Es un pago unico con acceso de por vida.</div></div>
</div>
<div class="fcta">
<h2>TU MARCA<br><span>NO PUEDE ESPERAR</span></h2>
<p style="color:#aaa;max-width:500px;margin:0 auto 32px;line-height:1.6;">Cada dia sin publicar es atencion que se lleva otro. Activa tu sistema y empieza a estar presente hoy.</p>
<a href="#planes" class="btn">🚀 QUIERO MI SISTEMA</a>
</div>
<footer>© 2026 TubeBot · Todos los derechos reservados<br>💬 Soporte: +34 656 691 085</footer>
</body>
</html>"""
# ══════════════════════════════════════════
# PÁGINA DE ACTIVACIÓN — DESPUÉS DEL PAGO
# ══════════════════════════════════════════
ACTIVAR_HTML = """<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TubeBot — Activa tu canal</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Manrope:wght@400;700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{background:#080808;color:#f0f0f0;font-family:'Manrope',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;}
.card{background:#111;border:1px solid #222;border-radius:20px;padding:40px 32px;max-width:480px;width:100%;text-align:center;}
.check-icon{font-size:3rem;margin-bottom:16px;}
h1{font-family:'Bebas Neue',sans-serif;font-size:2.5rem;color:#ff2d2d;margin-bottom:8px;}
.sub{color:#888;font-size:.95rem;margin-bottom:32px;line-height:1.6;}
label{display:block;text-align:left;font-size:.75rem;letter-spacing:2px;text-transform:uppercase;color:#ff2d2d;margin-bottom:6px;margin-top:16px;}
input,select{width:100%;background:#0a0a0a;border:1px solid #222;color:#f0f0f0;padding:14px 16px;border-radius:10px;font-family:'Manrope',sans-serif;font-size:.95rem;outline:none;}
input:focus,select:focus{border-color:#ff2d2d;}
.btn{width:100%;background:#ff2d2d;color:#fff;border:none;padding:16px;border-radius:10px;font-family:'Manrope',sans-serif;font-size:1rem;font-weight:800;cursor:pointer;margin-top:24px;transition:all .2s;}
.btn:hover{background:#cc0000;}
.btn:disabled{opacity:.5;cursor:not-allowed;}
.loading{display:none;margin-top:20px;color:#ff2d2d;font-size:.9rem;line-height:1.6;}
.result{display:none;margin-top:24px;background:#0a0a0a;border:1px solid #ff2d2d;border-radius:12px;padding:24px;text-align:left;}
.result h3{color:#ff2d2d;margin-bottom:12px;font-size:1.1rem;}
.result p{color:#888;font-size:.85rem;margin-bottom:8px;line-height:1.5;}
.url-box{background:#111;border:1px solid #333;border-radius:8px;padding:12px;word-break:break-all;font-size:.85rem;margin:12px 0;}
.url-box a{color:#fff;text-decoration:none;}
.copy-btn{background:transparent;border:1px solid #ff2d2d;color:#ff2d2d;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:.8rem;font-family:'Manrope',sans-serif;font-weight:700;width:100%;margin-top:8px;transition:all .2s;}
.copy-btn:hover{background:#ff2d2d;color:#fff;}
</style>
</head>
<body>
<div class="card">
<div class="check-icon">✅</div>
<h1>¡PAGO CONFIRMADO!</h1>
<p class="sub">Plan: <b style="color:#D4AF37;text-transform:uppercase">PLAN_PLACEHOLDER</b> · Puedes crear <b style="color:#ff2d2d">CANALES_PLACEHOLDER</b> canal(es)</p>
<label>Tu email</label>
<input type="email" id="email" placeholder="tu@email.com"/>
<div id="canales-container"></div>
<script>
const numCanales = CANALES_PLACEHOLDER;
const nichos = [
{v:"fitness y salud",t:"💪 Fitness y Salud"},
{v:"finanzas personales",t:"💰 Finanzas Personales"},
{v:"criptomonedas",t:"₿ Criptomonedas"},
{v:"tecnologia e inteligencia artificial",t:"🤖 Tecnología e IA"},
{v:"motivacion y exito personal",t:"🔥 Motivación y Éxito"},
{v:"mascotas",t:"🐾 Mascotas"},
{v:"recetas de cocina",t:"🍳 Cocina y Recetas"},
{v:"viajes y aventura",t:"✈️ Viajes"},
{v:"emprendimiento y negocios",t:"🚀 Emprendimiento"}
];
const maxCanales = 1;
const container = document.getElementById("canales-container");
for(let i=1; i<=maxCanales; i++){
const div = document.createElement("div");
div.style.cssText="background:#0d0d0d;border:1px solid #1e1e1e;border-radius:10px;padding:18px;margin-bottom:14px";
div.innerHTML = `
<h4 style="color:#D4AF37;margin:0 0 12px;font-size:1rem">🎯 Configura tu canal</h4>
<label style="font-size:.85rem;color:#aaa">Nicho de tu canal</label>
<input type="text" id="nicho${i}" placeholder="Ej: entrevistas en la calle, promocionar canal de YouTube, motivacion" list="nichos-sug" style="margin-bottom:8px;width:100%;padding:10px;background:#080808;color:#fff;border:1px solid #333;border-radius:8px"/>
<datalist id="nichos-sug">${nichos.map(n=>`<option value="${n.v}">`).join("")}</datalist>
<label style="font-size:.85rem;color:#aaa">Tipo de video</label>
<select id="tipo${i}">
<option value="largo">📺 Video Largo</option>
<option value="short">📱 Short Vertical</option>
</select>
<label style="font-size:.85rem;color:#aaa;margin-top:12px;display:block">✨ ¿Tienes alguna preferencia? <span style="color:#666">(opcional)</span></label>
<textarea id="config${i}" rows="4" placeholder="Ej: voz femenina en ingles, subtitulos grandes arriba, tono motivacional, sin musica..." style="width:100%;padding:12px;background:#080808;color:#fff;border:1px solid #333;border-radius:8px;resize:vertical;font-family:system-ui;margin-top:6px"></textarea>
<label style="font-size:.85rem;color:#aaa;margin-top:12px;display:block">📸 Sube tus imagenes o videos <span style="color:#666">(opcional, max 20)</span></label>
<input type="file" id="media${i}" multiple accept="image/*,video/*" style="margin-top:6px;width:100%;padding:10px;background:#080808;color:#fff;border:1px solid #333;border-radius:8px"/>
<div id="mediainfo${i}" style="color:#888;font-size:.8rem;margin-top:4px"></div>
`;
container.appendChild(div);
}
</script>
<button class="btn" id="btn" onclick="activar()">⚡ CREAR MI CANAL AUTOMÁTICO</button>
<div class="loading" id="loading">
⚡ Creando tu sistema en Hugging Face...<br>
<small style="color:#666;">Esto puede tardar 1-2 minutos</small>
</div>
<div class="result" id="result">
<h3>🎉 ¡Tu canal está listo!</h3>
<p>Accede aquí para ver tu sistema funcionando en tiempo real:</p>
<div class="url-box"><a id="url-link" href="#" target="_blank">Cargando...</a></div>
<button class="copy-btn" onclick="copiar()">📋 Copiar enlace</button>
<a id="yt-btn" href="#" target="_blank" class="btn" style="display:block;margin-top:12px;background:#ff0000;text-align:center;text-decoration:none;">📺 Conectar mi canal de YouTube</a>
<div style="margin-top:20px;background:#0d0d0d;border:1px solid #333;border-radius:12px;padding:20px;text-align:left;">
<p style="color:#ff2d2d;font-weight:700;margin-bottom:12px;">📋 GUÍA PASO A PASO:</p>
<p style="color:#888;font-size:.85rem;line-height:2;">
1️⃣ <b style="color:#fff;">Entra a tu URL</b> — Si pide login pulsa "Sign in as guest" o "Opciones avanzadas" para entrar sin cuenta<br>
2️⃣ <b style="color:#fff;">Espera 2-3 minutos</b> — La primera vez tarda en arrancar, es normal<br>
3️⃣ <b style="color:#fff;">Verás los logs</b> — Pantalla negra con texto dorado mostrando el bot en tiempo real<br>
4️⃣ <b style="color:#fff;">El bot trabaja solo</b> — Genera guión, voz, vídeo y lo sube a YouTube cada 4 horas<br>
5️⃣ <b style="color:#fff;">Sin tocar nada</b> — Funciona 24/7 completamente solo<br>
6️⃣ <b style="color:#fff;">¿Problemas?</b> — Escríbeme y te ayudo en minutos
</p>
<a href="https://wa.me/34656691085?text=Hola,%20necesito%20ayuda%20con%20mi%20TubeBot" style="display:block;margin-top:16px;background:#25d366;color:#fff;padding:12px;border-radius:8px;text-align:center;text-decoration:none;font-weight:700;">💬 Soporte WhatsApp</a>
</div>
</div>
</div>
<script>
async function activar(){
const email = document.getElementById('email').value.trim();
const numCanales = parseInt("CANALES_PLACEHOLDER") || 1;
const canales = [];
for(let i=1; i<=numCanales; i++){
const n = document.getElementById('nicho'+i);
const t = document.getElementById('tipo'+i);
const conf = document.getElementById('config'+i);
if(n && t) canales.push({nicho: n.value, tipo: t.value, config: conf ? conf.value : ""});
}
// Recoger archivos y convertir a base64
const archivos = [];
const fileInput = document.getElementById('media1');
if (fileInput && fileInput.files && fileInput.files.length > 0) {
document.getElementById('mediainfo1').textContent = "Procesando archivos...";
const files = Array.from(fileInput.files).slice(0, 20);
for (const f of files) {
try {
// Comprimir imagen antes de subir
const b64 = await new Promise((resolve, reject) => {
if (!f.type.startsWith('image/')) {
const r = new FileReader();
r.onload = () => resolve(r.result.split(',')[1]);
r.onerror = reject;
r.readAsDataURL(f);
return;
}
const img = new Image();
const url = URL.createObjectURL(f);
img.onload = () => {
const maxW = 1080;
let w = img.width, h = img.height;
if (w > maxW) { h = h * (maxW/w); w = maxW; }
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
const dataUrl = canvas.toDataURL('image/jpeg', 0.8);
URL.revokeObjectURL(url);
resolve(dataUrl.split(',')[1]);
};
img.onerror = reject;
img.src = url;
});
archivos.push({nombre: f.name.replace(/\.[^.]+$/, '.jpg'), contenido: b64});
} catch(e) { console.log("Error archivo", f.name, e); }
}
document.getElementById('mediainfo1').textContent = archivos.length + " archivos listos";
}
document.getElementById('btn').disabled = true;
document.getElementById('loading').style.display = 'block';
try{
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 300000);
const r = await fetch('/crear', {
method:'POST',
signal: ctrl.signal,
headers:{'Content-Type':'application/json'},
body: JSON.stringify({email, canales, archivos, token: "TOKEN_PLACEHOLDER"})
});
const d = await r.json();
if(d.url){
const emailVal = document.getElementById("email").value.trim();
window.location.href = "/mi-cuenta?email=" + encodeURIComponent(emailVal);
} else {
document.getElementById('btn').disabled = false;
}
}catch(e){
document.getElementById('btn').disabled = false;
}
document.getElementById('loading').style.display = 'none';
}
function copiar(){
}
</script>
</body>
</html>"""
# ══════════════════════════════════════════
# 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"👤 <b>Visita a landing</b>\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"💰 <b>POSIBLE VENTA</b>\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("<html><body style='background:#080808;color:#fff;text-align:center;padding:80px'><h1 style='color:#ff2d2d'>Acceso no autorizado</h1><a href='/' style='color:#ff2d2d'>Volver</a></body></html>", 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"🎬 <b>Canal creado</b>\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("<html><body style='background:#080808;color:#fff;text-align:center;padding:60px;font-family:sans-serif'><h1 style='color:#ff2d2d'>Cuenta no encontrada</h1><p>Verifica el email correcto.</p></body></html>", 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 = '<div style="margin-top:10px;color:#4caf50;font-size:.9rem">YouTube conectado</div>'
else:
yt_btn = f'<a href="/oauth-youtube?email={email}&canal={i-1}" style="display:inline-block;background:#ff0000;color:#fff;padding:10px 16px;border-radius:6px;text-decoration:none;font-weight:700;margin-top:10px;font-size:.9rem">Conectar YouTube</a>'
cfg_texto = ch.get("config_texto","").replace('"','&quot;').replace("\n"," ")
lista_html += f'''<div style="background:#111;border:1px solid #222;border-radius:10px;padding:18px;margin:10px 0"><div style="color:#D4AF37;font-weight:700;margin-bottom:8px">Canal {i}: {ch.get("nicho","-")}</div><a href="{ch_url}" target="_blank" style="color:#ff2d2d;word-break:break-all;font-size:.9rem">{ch_url}</a>{yt_btn}<div style="margin-top:10px"><button onclick="toggleEdit({i-1})" style="background:#222;color:#fff;border:1px solid #444;padding:8px 14px;border-radius:6px;font-size:.85rem;cursor:pointer">⚙️ Editar config</button></div><div id="edit{i-1}" style="display:none;margin-top:12px"><textarea id="cfg{i-1}" style="width:100%;background:#0a0a0a;color:#fff;border:1px solid #333;border-radius:8px;padding:10px;font-size:.9rem;min-height:80px" placeholder="Ej: voz femenina de Mexico, tono motivador, frase inicio Hola amigos">{cfg_texto}</textarea><button onclick="guardarCfg({i-1})" style="background:#4caf50;color:#fff;border:none;padding:10px 16px;border-radius:6px;font-weight:700;margin-top:8px;cursor:pointer">Guardar cambios</button><span id="msg{i-1}" style="margin-left:10px;font-size:.85rem"></span></div></div>'''
if not lista_html:
lista_html = '<p style="color:#888;text-align:center;padding:20px">Aun no has creado ningun canal</p>'
boton_nuevo = ""
if restantes > 0:
boton_nuevo = f'<a href="/activar-canal?token=micuenta_{email}" style="display:block;background:#ff2d2d;color:#fff;padding:18px;border-radius:10px;text-decoration:none;text-align:center;font-weight:900;margin:30px 0">+ CREAR NUEVO CANAL</a>'
TOKENS_VALIDOS[f"micuenta_{email}"] = {"plan": plan}
else:
boton_nuevo = '<p style="text-align:center;color:#D4AF37;padding:20px;background:#1a1400;border-radius:10px">Has alcanzado el limite de tu plan. Contacta soporte para subir de nivel.</p>'
return HTMLResponse(f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Mi Cuenta - TubeBot</title>
<style>body{{background:#080808;color:#fff;font-family:system-ui,sans-serif;margin:0;padding:30px 20px;max-width:700px;margin:0 auto}}h1{{color:#ff2d2d;font-size:2rem;margin:0 0 10px}}.plan{{display:inline-block;background:#D4AF37;color:#000;padding:4px 14px;border-radius:20px;font-weight:900;text-transform:uppercase;font-size:.85rem}}.stats{{background:#111;border:1px solid #222;border-radius:12px;padding:20px;margin:20px 0}}.stat-num{{font-size:2.5rem;color:#ff2d2d;font-weight:900}}.stat-lbl{{color:#888;font-size:.9rem}}</style></head>
<body>
<h1>🤖 Mi Cuenta TubeBot</h1>
<p style="color:#888;margin:0 0 16px">{email}</p>
<span class="plan">Plan {plan}</span>
<div class="stats">
<div style="display:flex;justify-content:space-between;text-align:center">
<div><div class="stat-num">{len(canales)}</div><div class="stat-lbl">Canales creados</div></div>
<div><div class="stat-num">{restantes}</div><div class="stat-lbl">Disponibles</div></div>
<div><div class="stat-num">{limite}</div><div class="stat-lbl">Limite plan</div></div>
</div>
</div>
{boton_nuevo}
<h2 style="font-size:1.3rem;margin-top:30px;color:#fff">Mis canales</h2>
{lista_html}
<p style="text-align:center;color:#666;margin-top:40px;font-size:.85rem">WhatsApp soporte: +34 656 691 085</p>
<script>
function toggleEdit(i){{var e=document.getElementById("edit"+i);e.style.display=e.style.display==="none"?"block":"none";}}
async function guardarCfg(i){{
var txt=document.getElementById("cfg"+i).value;
var msg=document.getElementById("msg"+i);
msg.textContent="Guardando...";msg.style.color="#D4AF37";
try{{
var r=await fetch("/editar-config",{{method:"POST",headers:{{"Content-Type":"application/json"}},body:JSON.stringify({{email:"{email}",canal_idx:i,config_texto:txt}})}});
var d=await r.json();
if(d.success){{msg.textContent="✅ Guardado. Se aplica en los proximos videos";msg.style.color="#4caf50";}}
else{{msg.textContent="❌ "+(d.error||"Error");msg.style.color="#ff2d2d";}}
}}catch(e){{msg.textContent="❌ Error de conexion";msg.style.color="#ff2d2d";}}
}}
</script>
</body></html>""")
def enviar_email_multi(email, urls):
try:
import requests
lista_urls = "".join([f'<li style="margin:8px 0"><a href="{u}" style="color:#ff2d2d">{u}</a></li>' for u in urls])
url_cuenta = f"https://cristobal299-iaweb.hf.space/mi-cuenta?email={email}"
html = f"""
<div style="background:#0a0a0a;color:#fff;padding:40px;font-family:sans-serif;max-width:600px;margin:0 auto;">
<h1 style="color:#ff2d2d;">TubeBot activado</h1>
<p>Tus <strong>{len(urls)} canales</strong> ya estan funcionando.</p>
<ul style="list-style:none;padding:0;">{lista_urls}</ul>
<p style="margin:30px 0;text-align:center;">
<a href="{url_cuenta}" style="background:#ff2d2d;color:#fff;padding:14px 28px;border-radius:6px;text-decoration:none;font-weight:bold;display:inline-block">Ir a Mi Cuenta</a>
</p>
<p style="color:#888;font-size:.85rem;text-align:center">Guarda este enlace para gestionar tus canales:<br><code style="color:#D4AF37;font-size:.8rem">{url_cuenta}</code></p>
<p style="color:#666;font-size:.85rem;margin-top:20px">Si tienes dudas responde este email.</p>
</div>"""
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"""<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TubeBot — Preparando tu canal...</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Manrope:wght@400;700&display=swap" rel="stylesheet">
<style>
*{{box-sizing:border-box;margin:0;padding:0;}}
body{{background:#080808;color:#f0f0f0;font-family:'Manrope',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;}}
.card{{background:#111;border:1px solid #222;border-radius:20px;padding:40px 32px;max-width:480px;width:100%;text-align:center;}}
h1{{font-family:'Bebas Neue',sans-serif;font-size:2.5rem;color:#ff2d2d;margin-bottom:16px;}}
.spinner{{width:60px;height:60px;border:4px solid #222;border-top:4px solid #ff2d2d;border-radius:50%;animation:spin 1s linear infinite;margin:24px auto;}}
@keyframes spin{{to{{transform:rotate(360deg);}}}}
p{{color:#888;font-size:.95rem;line-height:1.6;margin-bottom:12px;}}
.status{{color:#ff2d2d;font-weight:700;font-size:1rem;margin-top:16px;}}
.btn{{display:block;margin-top:24px;background:#ff2d2d;color:#fff;padding:14px;border-radius:10px;text-decoration:none;font-weight:800;font-family:'Bebas Neue',sans-serif;font-size:1.2rem;}}
</style>
</head>
<body>
<div class="card">
<h1>🤖 CREANDO TU CANAL</h1>
<div class="spinner"></div>
<p>Tu sistema está siendo construido en nuestros servidores.</p>
<p>Este proceso tarda entre <strong style="color:#fff;">3-5 minutos</strong> la primera vez.</p>
<p class="status" id="status">⚡ Instalando dependencias...</p>
<div style="background:#1a1a00;border:1px solid #D4AF37;border-radius:8px;padding:12px;margin-bottom:12px;font-size:.85rem;color:#D4AF37;text-align:left;">
⚠️ <b>Aviso normal de Google:</b> Verás "App no verificada". Es seguro. Pulsa <b>"Configuración avanzada"</b> y luego <b>"Ir a TubeBot (no seguro)"</b>. Es porque la app aún está en revisión por Google.
</div>
<a href="/oauth-youtube?email={email}" id="yt-btn" target="_blank" class="btn" style="display:none;background:#ff0000;margin-bottom:12px;">📺 Conectar YouTube</a>
<a href="{url}" class="btn" id="enlace" target="_blank" style="display:none;">🚀 ENTRAR A MI CANAL</a>
</div>
<script>
const url = "{url}";
const mensajes = [
"⚡ Instalando dependencias...",
"🧠 Configurando IA...",
"🎙️ Preparando síntesis de voz...",
"🎬 Configurando motor de video...",
"🔥 Casi listo...",
];
let i = 0;
const statusEl = document.getElementById('status');
const msgInterval = setInterval(() => {{
i = (i+1) % mensajes.length;
statusEl.textContent = mensajes[i];
}}, 8000);
async function checkReady() {{
try {{
const r = await fetch(`/check-space?url=${{encodeURIComponent(url)}}`); const d = await r.json(); if(!d.ready) throw new Error('not ready');
window.location.href = url;
}} catch(e) {{}}
}}
const checkInterval = setInterval(checkReady, 15000);
setTimeout(function(){{document.getElementById("yt-btn").style.display="block";}}, 180000);
setTimeout(function(){{window.location.href=url;}}, 270000);
</script>
</body>
</html>"""
@app.get("/test-llaves")
async def test_llaves():
return {
"GROQ_KEY_existe": bool(os.environ.get("GROQ_KEY","")),
"GROQ_KEY_longitud": len(os.environ.get("GROQ_KEY","")),
"PEXELS_KEY_existe": bool(os.environ.get("PEXELS_KEY","")),
"PEXELS_KEY_longitud": len(os.environ.get("PEXELS_KEY","")),
}
@app.get("/clientes")
async def ver_clientes():
return CLIENTES
@app.get("/test-token")
async def test_token():
import secrets
token = secrets.token_urlsafe(32)
TOKENS_VALIDOS[token] = "test@test.com"
return {"token": token, "url": f"/activar-canal?token={token}"}
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard():
total = len(CLIENTES)
conectados = sum(1 for c in CLIENTES.values() if c.get("youtube_conectado"))
filas = ""
for email, data in CLIENTES.items():
yt = "✅" if data.get("youtube_conectado") else "❌"
url = data.get("url","")
nicho = data.get("nicho","")
filas += f"<tr><td>{email}</td><td>{nicho}</td><td>{yt}</td><td><a href='{url}' target='_blank'>Ver Space</a></td></tr>"
return f"""<!DOCTYPE html>
<html><head><meta charset=UTF-8>
<style>
body{{background:#0a0a0a;color:#fff;font-family:sans-serif;padding:30px;}}
h1{{color:#ff2d2d;}}
.stats{{display:flex;gap:30px;margin:20px 0;}}
.stat{{background:#111;border:1px solid #333;border-radius:10px;padding:20px 30px;text-align:center;}}
.stat-n{{font-size:2.5rem;color:#ff2d2d;font-weight:bold;}}
.stat-l{{color:#666;font-size:.85rem;}}
table{{width:100%;border-collapse:collapse;margin-top:20px;}}
th{{background:#1a1a1a;padding:12px;text-align:left;color:#ff2d2d;}}
td{{padding:12px;border-bottom:1px solid #222;}}
a{{color:#ff2d2d;}}
</style></head>
<body>
<h1>🤖 TubeBot Dashboard</h1>
<div class="stats">
<div class="stat"><div class="stat-n">{total}</div><div class="stat-l">Clientes totales</div></div>
<div class="stat"><div class="stat-n">{conectados}</div><div class="stat-l">YouTube conectado</div></div>
<div class="stat"><div class="stat-n">{total-conectados}</div><div class="stat-l">Pendientes</div></div>
</div>
<table>
<tr><th>Email</th><th>Nicho</th><th>YouTube</th><th>Space</th></tr>
{filas}
</table>
</body></html>"""
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)
# ══════════════════════════════════════════
# YOUTUBE OAUTH
# ══════════════════════════════════════════
GOOGLE_PROYECTOS = [
# (os.environ.get("GOOGLE_CLIENT_ID",""), os.environ.get("GOOGLE_CLIENT_SECRET","")), # AGOTADO
# (os.environ.get("GOOGLE_CLIENT_ID_2",""), os.environ.get("GOOGLE_CLIENT_SECRET_2","")), # AGOTADO
# (os.environ.get("GOOGLE_CLIENT_ID_3",""), os.environ.get("GOOGLE_CLIENT_SECRET_3","")), # AGOTADO
# (os.environ.get("GOOGLE_CLIENT_ID_4",""), os.environ.get("GOOGLE_CLIENT_SECRET_4","")), # AGOTADO
# (os.environ.get("GOOGLE_CLIENT_ID_5",""), os.environ.get("GOOGLE_CLIENT_SECRET_5","")), # AGOTADO
(os.environ.get("GOOGLE_CLIENT_ID_6",""), os.environ.get("GOOGLE_CLIENT_SECRET_6","")),
(os.environ.get("GOOGLE_CLIENT_ID_7",""), os.environ.get("GOOGLE_CLIENT_SECRET_7","")),
(os.environ.get("GOOGLE_CLIENT_ID_8",""), os.environ.get("GOOGLE_CLIENT_SECRET_8","")),
(os.environ.get("GOOGLE_CLIENT_ID_9",""), os.environ.get("GOOGLE_CLIENT_SECRET_9","")),
]
GOOGLE_PROYECTOS = [(c,s) for c,s in GOOGLE_PROYECTOS if c and s]
import datetime as _dt
USOS_PROYECTO = {} # {idx: {"fecha": "2026-05-27", "count": 5}}
def reset_si_nuevo_dia(idx):
hoy = _dt.date.today().isoformat()
if idx not in USOS_PROYECTO or USOS_PROYECTO[idx].get("fecha") != hoy:
USOS_PROYECTO[idx] = {"fecha": hoy, "count": 0}
def incrementar_uso(idx):
reset_si_nuevo_dia(idx)
USOS_PROYECTO[idx]["count"] += 1
def proyecto_disponible():
"""Devuelve indice del proyecto con menos usos hoy (max 5 videos/dia)"""
if not GOOGLE_PROYECTOS:
return 0
contadores = []
for i in range(len(GOOGLE_PROYECTOS)):
reset_si_nuevo_dia(i)
contadores.append((USOS_PROYECTO[i]["count"], i))
contadores.sort()
return contadores[0][1] # el indice con menos usos
def get_proyecto_cliente(email):
"""Devuelve el proyecto Google asignado al cliente, o asigna uno nuevo"""
if email in CLIENTES and "google_index" in CLIENTES[email]:
idx = CLIENTES[email]["google_index"]
if idx < len(GOOGLE_PROYECTOS):
return GOOGLE_PROYECTOS[idx], idx
# Asignar el proyecto con menos USOS hoy (mejor reparto de cuota)
idx = proyecto_disponible()
return GOOGLE_PROYECTOS[idx], idx
# Default para arranque (se sobrescribe por cliente)
if GOOGLE_PROYECTOS:
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = GOOGLE_PROYECTOS[0]
else:
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET = "", ""
REDIRECT_URI = "https://cristobal299-iaweb.hf.space/oauth-callback"
SCOPES = "https://www.googleapis.com/auth/youtube.upload"
@app.get("/oauth-youtube")
async def oauth_youtube(email: str, canal: int = 0):
import urllib.parse
(cli_id, cli_sec), idx = get_proyecto_cliente(email)
if email in CLIENTES:
CLIENTES[email]["google_index"] = idx
guardar_clientes()
params = {
"client_id": cli_id,
"redirect_uri": REDIRECT_URI,
"response_type": "code",
"scope": SCOPES,
"access_type": "offline",
"prompt": "consent",
"state": f"{email}|{canal}"
}
url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params)
from fastapi.responses import RedirectResponse
return RedirectResponse(url)
@app.get("/oauth-callback")
async def oauth_callback(code: str, state: str):
import urllib.parse
if "|" in state:
email, canal_idx = state.split("|", 1)
canal_idx = int(canal_idx)
else:
email = state
canal_idx = 0
(cli_id, cli_sec), idx = get_proyecto_cliente(email)
async with httpx.AsyncClient() as client:
r = await client.post("https://oauth2.googleapis.com/token", data={
"code": code,
"client_id": cli_id,
"client_secret": cli_sec,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code"
})
token = r.json()
if "access_token" not in token:
return HTMLResponse("<h2>❌ Error al obtener token</h2>")
# Guardar token en el Space del cliente
if email in CLIENTES:
canales_lista = CLIENTES[email].get("canales", [])
space_repo = canales_lista[canal_idx].get("repo", "") if canal_idx < len(canales_lista) else ""
import json, base64
cuentas = [
(os.environ.get("HF_TOKEN_5",""), os.environ.get("HF_USER_5","Alejandrl")),
(os.environ.get("HF_TOKEN_4",""), os.environ.get("HF_USER_4","Maria288")),
(os.environ.get("HF_TOKEN_2",""), os.environ.get("HF_USER_2","Cristobal2999")),
(os.environ.get("HF_TOKEN_3",""), os.environ.get("HF_USER_3","Cristobal288")),
]
user_repo = space_repo.split("/")[0]
hf_token_repo = next((t for t,u in cuentas if u.lower()==user_repo.lower()), HF_TOKEN)
token_data = dict(token)
token_data["client_id"] = cli_id
token_data["client_secret"] = cli_sec
content_b64 = base64.b64encode(json.dumps(token_data).encode()).decode()
async with httpx.AsyncClient(timeout=30) as client:
r = await client.post(
f"https://huggingface.co/api/spaces/{space_repo}/commit/main",
headers={"Authorization": f"Bearer {hf_token_repo}", "Content-Type": "application/x-ndjson"},
content=f'{{"key":"header","value":{{"summary":"add token"}}}}\n{{"key":"file","value":{{"path":"token.json","content":"{content_b64}","encoding":"base64"}}}}\n'
)
print(f"Token upload: {r.status_code} {r.text[:200]}", flush=True)
# Reiniciar Space del cliente para que lea el token
r2 = await client.post(
f"https://huggingface.co/api/spaces/{space_repo}/restart?factory=true",
headers={"Authorization": f"Bearer {hf_token_repo}"}
)
print(f"Restart: {r2.status_code}", flush=True)
if canal_idx < len(CLIENTES[email].get("canales", [])):
CLIENTES[email]["canales"][canal_idx]["youtube"] = True
guardar_clientes()
url_cliente = f"https://cristobal299-iaweb.hf.space/mi-cuenta?email={email}"
nicho = CLIENTES.get(email, {}).get("nicho", "tu nicho")
return HTMLResponse(f"""
<html><head><meta charset="utf-8"><title>¡Bienvenido!</title></head>
<body style='background:#080808;color:#fff;font-family:system-ui;text-align:center;padding:40px 20px;max-width:600px;margin:0 auto'>
<div style='font-size:4rem'>🎉</div>
<h1 style='color:#ff2d2d;font-size:2.5rem;margin:20px 0'>¡BIENVENIDO A TUBEBOT!</h1>
<p style='color:#D4AF37;font-size:1.3rem;font-weight:700'>Tu canal de {nicho} está ACTIVO ✅</p>
<div style='background:#111;border:1px solid #333;border-radius:12px;padding:25px;margin:30px 0;text-align:left'>
<h3 style='color:#ff2d2d;margin-top:0'>🚀 Tu sistema ya está funcionando:</h3>
<p>✅ Generando guión con IA</p>
<p>✅ Voz profesional sintética</p>
<p>✅ Fondo de video automático</p>
<p>✅ Subtítulos animados</p>
<p>✅ Publica solo en YouTube cada 4 horas</p>
</div>
<a id="panel-btn" href="{url_cliente}" style='display:none;background:#ff0000;color:#fff;padding:20px;border-radius:10px;text-decoration:none;font-weight:800;font-size:1.2rem;margin:20px 0'>📺 IR A MI PANEL</a>
<div id="esperando-msg" style='background:#1a1a00;border:1px solid #D4AF37;border-radius:10px;padding:20px;margin:20px 0;color:#D4AF37'>⏳ Reiniciando tu sistema con YouTube conectado... <b>Espera 2 minutos.</b></div>
<script>setTimeout(()=>{{document.getElementById("panel-btn").style.display="block";document.getElementById("esperando-msg").style.display="none";}}, 120000);</script>
<p style='color:#888;margin-top:30px;font-size:0.9rem'>💡 Guarda esta URL en favoritos:<br><code style='color:#D4AF37'>{url_cliente}</code></p>
<p style='color:#888;margin-top:20px;font-size:0.9rem'>¿Problemas? WhatsApp: +34656691085</p>
</body></html>
""")
# 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"📺 <b>Video subido a YouTube</b>\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