Skip to content

Instantly share code, notes, and snippets.

@e-minguez
Created June 27, 2026 15:36
Show Gist options
  • Select an option

  • Save e-minguez/81a2a44441f28889fc8a16af83f2d296 to your computer and use it in GitHub Desktop.

Select an option

Save e-minguez/81a2a44441f28889fc8a16af83f2d296 to your computer and use it in GitHub Desktop.
Python small script to generate images on nan.builders using the image model
#!/usr/bin/env python3
"""nanweb — mini web local para generar imágenes con la API de NaN (flux-2-klein).
Sin dependencias: solo la librería estándar de Python 3.
La API key va en el servidor (este proceso), nunca en el navegador.
Uso:
export NAN_API_KEY=sk-tu-key-aqui
python3 nanweb.py
# abre solo http://localhost:8765 (Ctrl+C para parar)
"""
import base64
import json
import os
import threading
import urllib.error
import urllib.request
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
API_URL = "https://api.nan.builders/v1/images/generations"
MODEL = "flux-2-klein"
PORT = 8765
KEY = os.environ.get("NAN_API_KEY", "")
PAGE = """<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NaN Image — flux-2-klein</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background:#0f1115; color:#e6e6e6;
margin:0; padding:24px; display:flex; justify-content:center; }
.wrap { width:100%; max-width:720px; }
h1 { font-size:18px; font-weight:600; margin:0 0 16px; color:#bfc7d5; }
textarea { width:100%; min-height:84px; resize:vertical; background:#181b22;
color:#e6e6e6; border:1px solid #2a2f3a; border-radius:8px; padding:10px;
font-size:14px; font-family:inherit; }
.row { display:flex; gap:10px; align-items:center; margin:12px 0; flex-wrap:wrap; }
select, button { background:#181b22; color:#e6e6e6; border:1px solid #2a2f3a;
border-radius:8px; padding:8px 14px; font-size:14px; }
button.primary { background:#2563eb; border-color:#2563eb; cursor:pointer; font-weight:600; }
button.primary:disabled { opacity:.5; cursor:default; }
a.dl { color:#7aa2ff; text-decoration:none; font-size:14px; }
#status { color:#8a93a6; font-size:13px; min-height:18px; margin-left:auto; }
#out { margin-top:16px; }
#out img { max-width:100%; border-radius:10px; border:1px solid #2a2f3a; display:block; }
.err { color:#ff8080; white-space:pre-wrap; font-size:13px; }
</style>
</head>
<body>
<div class="wrap">
<h1>NaN Image · flux-2-klein</h1>
<textarea id="prompt" placeholder="Describe la imagen…">Un faro al atardecer sobre acantilados, estilo cinematico</textarea>
<div class="row">
<select id="size">
<option>1024x1024</option>
<option>1536x1024</option>
<option>1024x1536</option>
</select>
<button id="go" class="primary">Generar</button>
<span id="status"></span>
</div>
<div id="out"></div>
</div>
<script>
const go = document.getElementById('go');
const statusEl = document.getElementById('status');
const out = document.getElementById('out');
go.addEventListener('click', async () => {
const prompt = document.getElementById('prompt').value.trim();
const size = document.getElementById('size').value;
if (!prompt) { statusEl.textContent = 'Escribe un prompt.'; return; }
go.disabled = true; statusEl.textContent = 'Generando…'; out.innerHTML = '';
try {
const r = await fetch('/generate', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ prompt, size })
});
const data = await r.json();
if (!r.ok || data.error) {
out.innerHTML = '<div class="err">' + (data.error || ('HTTP ' + r.status)) + '</div>';
statusEl.textContent = 'Error.';
} else {
const src = 'data:' + data.mime + ';base64,' + data.b64;
out.innerHTML = '<img src="' + src + '"><div class="row"><a class="dl" download="nan-image.' +
data.ext + '" href="' + src + '">Descargar</a></div>';
statusEl.textContent = 'Listo.';
}
} catch (e) {
out.innerHTML = '<div class="err">' + e + '</div>';
statusEl.textContent = 'Error.';
} finally { go.disabled = false; }
});
</script>
</body>
</html>"""
def generate(prompt, size):
body = {
"model": MODEL,
"prompt": prompt,
"n": 1,
"size": size,
"response_format": "b64_json",
}
req = urllib.request.Request(
API_URL,
data=json.dumps(body).encode(),
headers={
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
# Cloudflare delante de NaN da 403 (error 1010) con el UA por defecto
# de urllib; mandamos uno de navegador para que no nos banee.
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36",
"Accept": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
data = json.load(resp)
raw = base64.b64decode(data["data"][0]["b64_json"])
if raw[:3] == b"\xff\xd8\xff":
return raw, "image/jpeg", "jpg"
return raw, "image/png", "png"
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass # silencio
def _send(self, code, body, ctype="application/json"):
self.send_response(code)
self.send_header("Content-Type", ctype)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path in ("/", "/index.html"):
self._send(200, PAGE.encode(), "text/html; charset=utf-8")
else:
self._send(404, b"not found", "text/plain")
def do_POST(self):
if self.path != "/generate":
self._send(404, json.dumps({"error": "not found"}).encode())
return
try:
length = int(self.headers.get("Content-Length", 0))
payload = json.loads(self.rfile.read(length) or b"{}")
prompt = (payload.get("prompt") or "").strip()
size = payload.get("size") or "1024x1024"
if not KEY:
raise RuntimeError("Falta NAN_API_KEY en el entorno del servidor.")
if not prompt:
raise RuntimeError("Prompt vacío.")
raw, mime, ext = generate(prompt, size)
b64 = base64.b64encode(raw).decode()
self._send(200, json.dumps({"b64": b64, "mime": mime, "ext": ext}).encode())
except urllib.error.HTTPError as e:
detail = e.read().decode(errors="replace")
self._send(200, json.dumps({"error": f"HTTP {e.code}: {detail}"}).encode())
except Exception as e: # noqa: BLE001
self._send(200, json.dumps({"error": str(e)}).encode())
def main():
if not KEY:
print("AVISO: NAN_API_KEY no está definida. Expórtala antes de generar.")
url = f"http://localhost:{PORT}"
print(f"NaN Image en {url} (Ctrl+C para parar)")
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
threading.Timer(0.6, lambda: webbrowser.open(url)).start()
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nAdiós.")
server.shutdown()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment