generador-gratis / appppp.py
Cristobal299's picture
Rename app.py to appppp.py
0faf0ff verified
Raw
History Blame Contribute Delete
11.9 kB
"""
Fénix — Generador Gratis
Space público, SIN login, que genera una página web con IA (Cerebras -> Groq
de respaldo) y la despliega directo a Vercel. No toca La Forja de pago:
motor y despliegue independientes, solo comparte el mismo estilo de prompt.
Variables de entorno necesarias en este Space (Settings -> Variables and secrets):
CEREBRAS_KEY = tu API key de Cerebras (motor principal)
GROQ_KEY = tu API key de Groq (respaldo si Cerebras falla)
VERCEL_TOKEN = tu token de Vercel (Account Settings -> Tokens)
LIMITE_DIA = opcional, nº máximo de webs gratis por día (default 15)
"""
import os
import re
import json
import base64
import requests
import gradio as gr
from datetime import datetime, timezone
from openai import OpenAI
# ─────────────── CONFIG ───────────────
CEREBRAS_KEY = os.environ.get("CEREBRAS_KEY", "")
GROQ_KEY = os.environ.get("GROQ_KEY", "")
VERCEL_TOKEN = os.environ.get("VERCEL_TOKEN", "")
LIMITE_DIA = int(os.environ.get("LIMITE_DIA", "15"))
CEREBRAS_MODEL = "gpt-oss-120b"
GROQ_MODEL = "openai/gpt-oss-20b"
_cerebras = OpenAI(api_key=CEREBRAS_KEY, base_url="https://api.cerebras.ai/v1") if CEREBRAS_KEY else None
_groq = OpenAI(api_key=GROQ_KEY, base_url="https://api.groq.com/openai/v1") if GROQ_KEY else None
CONTADOR_FILE = "contador_dia.json"
# ─────────────── LÍMITE DIARIO (protección anti-abuso, sin login) ───────────────
def _leer_contador():
hoy = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if os.path.exists(CONTADOR_FILE):
try:
with open(CONTADOR_FILE, "r") as f:
data = json.load(f)
except Exception:
data = {}
else:
data = {}
if data.get("dia") != hoy:
data = {"dia": hoy, "usadas": 0}
return data
def _incrementar_contador():
data = _leer_contador()
data["usadas"] += 1
with open(CONTADOR_FILE, "w") as f:
json.dump(data, f)
return data["usadas"]
def _cupo_disponible():
data = _leer_contador()
return data["usadas"] < LIMITE_DIA, data["usadas"]
# ─────────────── MOTOR IA: Cerebras -> Groq ───────────────
def _chat_raw(messages, temperature=0.3, max_tokens=12000):
errores = []
intentos = []
if _cerebras:
intentos.append(("Cerebras", _cerebras, CEREBRAS_MODEL))
if _groq:
intentos.append(("Groq", _groq, GROQ_MODEL))
if not intentos:
raise RuntimeError("No hay CEREBRAS_KEY ni GROQ_KEY configuradas en este Space.")
for nombre, cliente, modelo in intentos:
try:
print(f"[Generador] Probando motor: {nombre}", flush=True)
resp = cliente.chat.completions.create(
model=modelo,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
)
return resp.choices[0].message.content
except Exception as e:
print(f"[Generador] {nombre} falló: {e}", flush=True)
errores.append(f"{nombre}: {e}")
raise RuntimeError("Todos los motores fallaron:\n" + "\n".join(errores))
# ─────────────── PARSEO (mismo formato que La Forja) ───────────────
def _parsear_respuesta(txt):
m = re.search(r"SPACE:\s*([^\n]+)", txt)
space_name = m.group(1).strip() if m else "web-generada"
space_name = re.sub(r"[^a-z0-9-]+", "-", space_name.lower()).strip("-") or "web-generada"
files = []
patron = re.compile(
r"((?:[A-Za-z0-9_./-]+\.(?:html|css|js|json|md|txt))|Dockerfile)[^\n]*\n+```[^\n]*\n(.*?)(?:```|\Z)",
re.DOTALL,
)
for path, contenido in patron.findall(txt):
files.append({"path": path.strip(), "content": contenido.strip("\n")})
if not files:
muestra = " ".join(txt.split())[:300]
raise ValueError(f"No se encontraron archivos en la respuesta del modelo: «{muestra}…»")
return {"space_name": space_name, "files": files}
# ─────────────── PROMPT (recortado del SYSTEM_WEB de La Forja) ───────────────
SYSTEM_WEB = """Eres el mejor diseñador web del mundo. Cada web que creas es una obra de arte única, visualmente impactante, que deja al cliente sin palabras. No existe el "suficientemente bueno" para ti.
Responde listando los archivos con su nombre y un bloque de código. Mínimo index.html. Así:
SPACE: nombre-corto-en-minusculas-con-guiones
### index.html
```html
<!doctype html> ... página completa con CSS en <style> y JS en <script> ...
```
REGLAS DE DISEÑO VISUAL (obligatorias, sin excepción):
- Una sola página index.html autocontenida: CSS dentro de <style>, JS dentro de <script>. Sin frameworks ni build.
- HERO ESPECTACULAR: fondo con gradiente o imagen de fondo con overlay de color, título grande en negrita con sombra o gradiente de texto, subtítulo claro, botón CTA llamativo con efecto hover. El hero ocupa al menos el 80% de la pantalla.
- PALETA DE COLORES ÚNICA: elige una paleta de 2-3 colores que encaje con el sector del negocio. Nunca gris plano ni blanco puro.
- TIPOGRAFÍA CON PERSONALIDAD: importa 2 fuentes de Google Fonts (una display para títulos, una legible para texto). Tamaños grandes y audaces para títulos (3rem+).
- ANIMACIONES SUTILES: fade-in al hacer scroll, hover en tarjetas, botones con efecto pulse o glow.
- TARJETAS DE SERVICIOS: si hay fotos reales disponibles (rutas exactas indicadas), úsalas como <img> real, no icono. Si no hay fotos, usa icono SVG o emoji grande.
- NAVBAR fija con fondo semitransparente (backdrop-filter: blur). En móvil: menú hamburguesa.
- FOOTER completo: logo, descripción corta, datos de contacto, copyright.
- RESPONSIVE perfecto: mobile-first, sin scroll horizontal.
- Copys reales, directos y persuasivos en español. Nada de Lorem ipsum.
- EVITA los clichés de IA: fondo crema+terracota, negro+verde ácido, estilo periódico con líneas finas. Sé original.
REGLAS DE CONTENIDO (OBLIGATORIAS):
- Moneda: precios en la moneda del país del negocio. Si es LATAM usa $. Si es España usa €.
- Contacto: NUNCA inventes teléfono, email, WhatsApp ni dirección. Si falta algún dato escribe [TU WHATSAPP], [TU EMAIL] como placeholder visible.
- WhatsApp: enlace siempre con prefijo de país sin espacios: https://wa.me/XXXXXXXXXXX
- Testimonios: NO inventes testimonios. Si no hay reales, OMITE esa sección.
- Botón WhatsApp flotante: SIEMPRE añade un botón verde flotante en la esquina inferior derecha.
REGLAS TÉCNICAS:
- Pon SIEMPRE el nombre del archivo antes de su bloque de código.
- El HTML final debe tener MÍNIMO 250 líneas de código. Una web corta es una web mala.
"""
# ─────────────── DESPLIEGUE A VERCEL (de deploy_multi.py) ───────────────
def desplegar_vercel(files: dict, name: str, vercel_token: str, log=print) -> dict:
headers = {
"Authorization": f"Bearer {vercel_token}",
"Content-Type": "application/json",
}
archivos = []
for ruta, contenido in files.items():
if isinstance(contenido, bytes):
archivos.append({"file": ruta,
"data": base64.b64encode(contenido).decode(),
"encoding": "base64"})
else:
archivos.append({"file": ruta, "data": contenido, "encoding": "utf-8"})
body = {
"name": name,
"files": archivos,
"projectSettings": {"framework": None},
"target": "production",
}
r = requests.post("https://api.vercel.com/v13/deployments",
headers=headers, json=body)
if r.status_code not in (200, 201):
raise RuntimeError(f"[Vercel] Error creando deployment ({r.status_code}): {r.text[:400]}")
j = r.json()
url = j.get("url", "")
if url and not url.startswith("http"):
url = "https://" + url
log(f"[Vercel] Desplegado: {url}")
# Desactivar Vercel Authentication (candado) para que la web quede pública.
try:
r2 = requests.patch(
f"https://api.vercel.com/v9/projects/{name}",
headers=headers,
json={"ssoProtection": None},
)
if r2.status_code in (200, 201):
log(f"[Vercel] Candado (Vercel Authentication) desactivado para {name}")
else:
log(f"[Vercel] Aviso: no se pudo desactivar el candado ({r2.status_code}): {r2.text[:200]}")
except Exception as e:
log(f"[Vercel] Aviso: fallo al intentar desactivar el candado: {e}")
return {"_forja_url": url, "deploy_id": j.get("id")}
# ─────────────── FUNCIÓN PRINCIPAL (la que llama el frontend) ───────────────
def generar_web(objetivo, imagen=None):
if not objetivo or not objetivo.strip():
return "⚠️ Escribe una descripción de tu negocio primero."
ok, usadas = _cupo_disponible()
if not ok:
return (f"🙏 Hoy ya se generaron {usadas} webs gratis (límite diario). "
f"Vuelve mañana, o escribe por WhatsApp para una versión sin espera.")
instruccion = f"Crea una página web profesional y bonita para: {objetivo.strip()}"
files_extra = {}
if imagen is not None:
try:
ext = ".jpg"
ruta_web = f"static/img/imagen_1{ext}"
with open(imagen, "rb") as fh:
files_extra[ruta_web] = fh.read()
instruccion += (
f"\n\nIMPORTANTE: ya hay 1 foto real subida en esta ruta exacta: {ruta_web}. "
f"OBLIGATORIO: úsala como <img src=\"{ruta_web}\"> en el hero de fondo y/o "
f"en la tarjeta principal de servicios. No uses imágenes placeholder."
)
except Exception as e:
print(f"[Generador] No pude leer la imagen subida: {e}")
messages = [
{"role": "system", "content": SYSTEM_WEB},
{"role": "user", "content": instruccion},
]
try:
contenido = _chat_raw(messages)
data = _parsear_respuesta(contenido)
except Exception as e:
return f"❌ Falló generando la web: {e}"
files_dict = {f["path"]: f["content"] for f in data["files"]}
files_dict.update(files_extra)
if not VERCEL_TOKEN:
return "❌ Falta VERCEL_TOKEN en las variables de entorno de este Space."
try:
res = desplegar_vercel(files_dict, data["space_name"], VERCEL_TOKEN)
url = res.get("_forja_url") or f"https://{data['space_name']}.vercel.app"
except Exception as e:
return f"❌ Falló el despliegue en Vercel: {e}"
_incrementar_contador()
return f"🌐 ¡Tu página está lista!\n\n{url}\n\n(Vercel tarda ~20-30s en activarse la primera vez)"
# ─────────────── INTERFAZ GRADIO (con api_name fijo para el frontend) ───────────────
with gr.Blocks(title="Fénix · Generador Gratis") as demo:
gr.Markdown("## 🔥 Fénix — Generador Gratis\nDescribe tu negocio, adjunta una foto si quieres, y recibe tu web en segundos.")
objetivo_in = gr.Textbox(label="Describe tu negocio", lines=3,
placeholder="Ej: cafetería en La Línea, ambiente cálido, especialidad en tostadas y zumos naturales, WhatsApp 34600000000")
imagen_in = gr.Image(label="Foto (opcional)", type="filepath")
btn = gr.Button("🚀 Generar mi web", variant="primary")
salida = gr.Textbox(label="Resultado", lines=4, interactive=False)
btn.click(generar_web, [objetivo_in, imagen_in], [salida], api_name="generar_web")
demo.launch()