Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

see: free FLUX image gen piece (NVIDIA NIM proxy)

Adds the `see` piece that generates images via NVIDIA's free FLUX.1
schnell endpoint. Two filter-safe AC style presets baked into the proxy
(kidlisp = high-contrast CRT energy, warm = soft pastel mascot). 30s
timeout, graceful safety-filter handling, friendly error messages.

URL forms:
see a happy frog — kidlisp preset, random seed
see:warm a coffee mug — warm pastel preset
see:raw a misty forest — no AC style suffix
Tap to roll a new seed, backspace to re-prompt.

Backend: system/netlify/functions/flux.mjs proxies to
ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-schnell at 768/4
(verified ~1.3s warm direct, ~3.5s through Node fetch). Returns the JPEG
as a data URL; piece decodes via Image() + canvas readback into a
paste-able bitmap. Filter-safe prompt suffixes encoded with comments
explaining the bisect that found them — NVIDIA's safety classifier is
twitchy about clusters of proper nouns, so the suffixes deliberately
avoid naming the platform / maker / language.

Requires NVIDIA_API_KEY (already in the vault root .env, but not yet
propagated to lith/.env — production wiring deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+365
+4
system/netlify.toml
··· 2058 2058 # status = 200 2059 2059 # force = false 2060 2060 [[redirects]] 2061 + from = "/api/flux" 2062 + to = "/.netlify/functions/flux" 2063 + status = 200 2064 + [[redirects]] 2061 2065 from = "/api/playlist" 2062 2066 to = "/.netlify/functions/playlist" 2063 2067 status = 200
+167
system/netlify/functions/flux.mjs
··· 1 + // flux, 26.04.23 2 + // Proxy to NVIDIA NIM FLUX.1 schnell image generation. 3 + // Hides the NVIDIA_API_KEY, applies one of two AC style presets, 4 + // returns the raw JPEG as a data URL the piece can decode directly. 5 + // 6 + // Usage from a piece: 7 + // const res = await fetch("/api/flux", { 8 + // method: "POST", 9 + // headers: { "Content-Type": "application/json" }, 10 + // body: JSON.stringify({ prompt: "a happy frog", preset: "kidlisp", seed: 7 }), 11 + // }); 12 + // const { ok, png, reason, elapsed_ms, seed } = await res.json(); 13 + // 14 + // On safety-filter rejection: { ok: false, reason: "filtered" } (200, so the 15 + // piece can react gracefully). On NVIDIA upstream error: 502. 16 + // 17 + // Env: NVIDIA_API_KEY (required). Lives in lith/.env in production. 18 + 19 + import { respond } from "../../backend/http.mjs"; 20 + 21 + const FLUX_URL = 22 + "https://ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-schnell"; 23 + 24 + // Two filter-safe AC style suffixes. The bisect that pinned these down lives 25 + // in ~/Desktop/nvidia-flux-log/README.md — short version: NVIDIA's safety 26 + // classifier filters on clusters of proper nouns + dense modifiers, so the 27 + // suffixes deliberately avoid naming the platform / maker / language. 28 + const PRESETS = { 29 + // Soft pastel mascot energy — animals, food, friendly subjects 30 + warm: 31 + "chunky pixel-art bitmap, crisp 1-pixel edges, no anti-aliasing, " + 32 + "saturated palette of black, navy, hot pink, lime, cyan, yellow, magenta, white, " + 33 + "centered subject on flat solid color background, " + 34 + "soft 1-pixel offset pastel shadow beneath subject, " + 35 + "square mobile composition, 90s indie computing aesthetic, " + 36 + "handmade lo-fi warmth, no text, no UI, no watermarks", 37 + 38 + // High-contrast CRT energy — devices, abstract objects, default 39 + kidlisp: 40 + "high-contrast pixel-art bitmap, crisp 1-pixel edges, no anti-aliasing, " + 41 + "strict palette of black, hot pink, lime, cyan, yellow, white, " + 42 + "solid black background, " + 43 + "hard cyan 1-pixel shadow beneath subject, " + 44 + "square composition, no text", 45 + 46 + // No styling — pass the user's prompt through verbatim 47 + raw: "", 48 + }; 49 + 50 + const ALLOWED_WIDTHS = [768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344]; 51 + 52 + export async function handler(event) { 53 + if (event.httpMethod === "OPTIONS") { 54 + return respond(200, ""); 55 + } 56 + if (event.httpMethod !== "POST") { 57 + return respond(405, { ok: false, reason: "method" }); 58 + } 59 + 60 + if (!process.env.NVIDIA_API_KEY) { 61 + console.error("flux: NVIDIA_API_KEY not configured"); 62 + return respond(500, { ok: false, reason: "no_key" }); 63 + } 64 + 65 + let body; 66 + try { 67 + body = JSON.parse(event.body || "{}"); 68 + } catch { 69 + return respond(400, { ok: false, reason: "bad_json" }); 70 + } 71 + 72 + const prompt = (body.prompt || "").toString().trim(); 73 + if (!prompt) return respond(400, { ok: false, reason: "no_prompt" }); 74 + if (prompt.length > 1000) 75 + return respond(400, { ok: false, reason: "prompt_too_long" }); 76 + 77 + const presetName = body.preset || "kidlisp"; 78 + const styleSuffix = PRESETS[presetName] ?? PRESETS.kidlisp; 79 + const fullPrompt = styleSuffix ? `${prompt} — ${styleSuffix}` : prompt; 80 + 81 + // Width/height clamp to FLUX's literal allowed set. Default 768 (smallest 82 + // → fastest, most reliable). Pieces that want bigger pay the latency tail. 83 + const width = ALLOWED_WIDTHS.includes(+body.width) ? +body.width : 768; 84 + const height = ALLOWED_WIDTHS.includes(+body.height) ? +body.height : width; 85 + 86 + const seed = Number.isInteger(body.seed) 87 + ? body.seed 88 + : Math.floor(Math.random() * 1e9); 89 + 90 + // 30s timeout — FLUX schnell normally returns in 1-4s. NVIDIA has been 91 + // observed hanging for minutes before 504'ing during outages; fail fast 92 + // so the piece can show an error and let the user retry. 93 + const controller = new AbortController(); 94 + const timeoutId = setTimeout(() => controller.abort(), 30000); 95 + 96 + const t0 = Date.now(); 97 + let upstream; 98 + try { 99 + upstream = await fetch(FLUX_URL, { 100 + method: "POST", 101 + headers: { 102 + Authorization: `Bearer ${process.env.NVIDIA_API_KEY}`, 103 + "Content-Type": "application/json", 104 + Accept: "application/json", 105 + }, 106 + body: JSON.stringify({ 107 + prompt: fullPrompt, 108 + cfg_scale: 0, 109 + steps: 4, 110 + seed, 111 + width, 112 + height, 113 + mode: "base", 114 + }), 115 + signal: controller.signal, 116 + }); 117 + } catch (err) { 118 + if (err.name === "AbortError") { 119 + return respond(504, { ok: false, reason: "timeout" }); 120 + } 121 + console.error("flux: upstream fetch failed", err); 122 + return respond(502, { ok: false, reason: "network", detail: err.message }); 123 + } finally { 124 + clearTimeout(timeoutId); 125 + } 126 + 127 + if (!upstream.ok) { 128 + const detail = await upstream.text().catch(() => ""); 129 + console.error("flux: upstream", upstream.status, detail.slice(0, 300)); 130 + return respond(502, { 131 + ok: false, 132 + reason: "upstream", 133 + status: upstream.status, 134 + detail: detail.slice(0, 300), 135 + }); 136 + } 137 + 138 + let data; 139 + try { 140 + data = await upstream.json(); 141 + } catch { 142 + return respond(502, { ok: false, reason: "bad_upstream_json" }); 143 + } 144 + 145 + const art = data?.artifacts?.[0]; 146 + if (!art) return respond(502, { ok: false, reason: "no_artifact" }); 147 + 148 + if (art.finishReason !== "SUCCESS") { 149 + // Safety filter — return 200 so the piece can react. 150 + return respond(200, { 151 + ok: false, 152 + reason: "filtered", 153 + finish: art.finishReason, 154 + }); 155 + } 156 + 157 + const elapsed_ms = Date.now() - t0; 158 + return respond(200, { 159 + ok: true, 160 + png: `data:image/jpeg;base64,${art.base64}`, 161 + width, 162 + height, 163 + seed: art.seed, 164 + preset: presetName, 165 + elapsed_ms, 166 + }); 167 + }
+194
system/public/aesthetic.computer/disks/see.mjs
··· 1 + // see, 26.04.23 2 + // Free image generation via NVIDIA NIM FLUX.1 schnell, with two AC style 3 + // presets baked into the proxy at /api/flux. Drop a prompt and a bitmap 4 + // shows up — the model does the rest. 5 + // 6 + // Usage: 7 + // see — show usage 8 + // see a happy frog — generate with default kidlisp preset 9 + // see:warm a happy frog — soft pastel mascot preset 10 + // see:raw photorealistic frog — no AC style suffix, raw FLUX 11 + // 12 + // Tap to roll a new seed. Backspace to clear and re-prompt. 13 + 14 + const { floor, min, max } = Math; 15 + 16 + let state = "empty"; // "empty" | "loading" | "ready" | "error" 17 + let promptText = ""; 18 + let presetName = "kidlisp"; 19 + let bitmap = null; // { width, height, pixels: Uint8ClampedArray } 20 + let errorMsg = ""; 21 + let seedNum = null; // null = let server roll 22 + let elapsedMs = 0; 23 + let ellipsis = 0; 24 + let frame = 0; 25 + let abortController = null; 26 + 27 + function boot({ params, colon, hud }) { 28 + hud.label("see"); 29 + if (colon[0]) presetName = colon[0]; 30 + promptText = (params || []).join(" ").trim(); 31 + if (promptText) generate(); 32 + } 33 + 34 + function meta() { 35 + return { 36 + title: "see", 37 + desc: "Free FLUX image generation in your AC palette.", 38 + }; 39 + } 40 + 41 + async function generate() { 42 + if (!promptText) return; 43 + state = "loading"; 44 + bitmap = null; 45 + errorMsg = ""; 46 + ellipsis = 0; 47 + 48 + abortController?.abort(); 49 + abortController = new AbortController(); 50 + 51 + try { 52 + const res = await fetch("/api/flux", { 53 + method: "POST", 54 + headers: { "Content-Type": "application/json" }, 55 + body: JSON.stringify({ 56 + prompt: promptText, 57 + preset: presetName, 58 + ...(seedNum !== null ? { seed: seedNum } : {}), 59 + }), 60 + signal: abortController.signal, 61 + }); 62 + 63 + const data = await res.json(); 64 + if (!data.ok) { 65 + state = "error"; 66 + errorMsg = 67 + data.reason === "filtered" 68 + ? "blocked by safety filter — try different wording" 69 + : data.reason === "no_key" 70 + ? "server has no NVIDIA_API_KEY" 71 + : data.reason === "timeout" 72 + ? "timed out — NVIDIA may be slow, tap to retry" 73 + : data.reason === "upstream" 74 + ? "NVIDIA error — tap to retry" 75 + : `error: ${data.reason || "unknown"}`; 76 + return; 77 + } 78 + 79 + elapsedMs = data.elapsed_ms; 80 + seedNum = parseInt(data.seed, 10); 81 + bitmap = await dataUrlToBitmap(data.png); 82 + state = "ready"; 83 + } catch (err) { 84 + if (err.name === "AbortError") return; 85 + state = "error"; 86 + errorMsg = err.message; 87 + } 88 + } 89 + 90 + // Decode a data:image/jpeg;base64,... URL into an AC-paste-able bitmap. 91 + function dataUrlToBitmap(dataUrl) { 92 + return new Promise((resolve, reject) => { 93 + const img = new Image(); 94 + img.onload = () => { 95 + const canvas = document.createElement("canvas"); 96 + canvas.width = img.width; 97 + canvas.height = img.height; 98 + const ctx = canvas.getContext("2d"); 99 + ctx.drawImage(img, 0, 0); 100 + const id = ctx.getImageData(0, 0, img.width, img.height); 101 + resolve({ width: img.width, height: img.height, pixels: id.data }); 102 + }; 103 + img.onerror = (e) => reject(new Error("decode failed")); 104 + img.src = dataUrl; 105 + }); 106 + } 107 + 108 + function paint({ wipe, ink, paste, write, screen }) { 109 + frame++; 110 + const w = screen.width; 111 + const h = screen.height; 112 + 113 + // Black background — matches the kidlisp preset's own background, looks 114 + // intentional regardless of preset. 115 + wipe(0); 116 + 117 + if (state === "ready" && bitmap) { 118 + // Center, scale-to-fit with integer scale (preserves pixel crispness). 119 + const scale = max(1, floor(min(w / bitmap.width, h / bitmap.height))); 120 + const drawW = bitmap.width * scale; 121 + const drawH = bitmap.height * scale; 122 + const x = floor((w - drawW) / 2); 123 + const y = floor((h - drawH) / 2); 124 + paste(bitmap, x, y, { scale }); 125 + 126 + // Subtle status footer 127 + const footer = `${elapsedMs}ms · seed ${seedNum} · ${presetName}`; 128 + ink(80).write(footer, { x: 6, y: h - 14 }); 129 + ink(180).write("tap to roll", { x: w - 70, y: h - 14 }); 130 + return; 131 + } 132 + 133 + if (state === "loading") { 134 + if (frame % 20 === 0) ellipsis = (ellipsis + 1) % 4; 135 + const dots = ".".repeat(ellipsis); 136 + ink(0, 255, 200).write(`generating${dots}`, { center: "xy" }); 137 + ink(80).write(promptText, { center: "x", y: floor(h / 2) + 18 }); 138 + return; 139 + } 140 + 141 + if (state === "error") { 142 + ink(255, 80, 120).write("✗", { center: "x", y: floor(h / 2) - 20 }); 143 + ink(255, 200, 200).write(errorMsg, { center: "xy" }, undefined, w - 20); 144 + ink(120).write("tap to retry", { center: "x", y: floor(h / 2) + 24 }); 145 + return; 146 + } 147 + 148 + // empty — show usage 149 + const lines = [ 150 + "type a subject to see it", 151 + "", 152 + "see a happy frog", 153 + "see:warm a coffee mug", 154 + "see:raw a misty forest", 155 + ]; 156 + let yy = floor(h / 2) - (lines.length * 14) / 2; 157 + for (const line of lines) { 158 + ink(line.startsWith("see") ? [0, 255, 200] : 200).write(line, { 159 + center: "x", 160 + y: yy, 161 + }); 162 + yy += 14; 163 + } 164 + } 165 + 166 + function act({ event: e, sound }) { 167 + if (state === "loading") return; 168 + 169 + if (e.is("touch")) { 170 + if (state === "error") { 171 + // retry with same seed 172 + generate(); 173 + } else if (state === "ready") { 174 + // roll a new seed 175 + seedNum = null; 176 + sound?.synth?.({ type: "sine", tone: 660, duration: 0.04, volume: 0.3 }); 177 + generate(); 178 + } 179 + } 180 + 181 + if (e.is("keyboard:down:backspace") || e.is("keyboard:down:escape")) { 182 + state = "empty"; 183 + bitmap = null; 184 + errorMsg = ""; 185 + abortController?.abort(); 186 + } 187 + } 188 + 189 + function leave() { 190 + abortController?.abort(); 191 + bitmap = null; 192 + } 193 + 194 + export { boot, paint, act, leave, meta };