""" 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 ... página completa con CSS en