From 680ccbed68d676837589c9516cdbcf3780ca0bf0 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 7 Jun 2026 20:20:27 -0700 Subject: [PATCH] =?UTF-8?q?keeps:=20series=20pipeline=20(generate=20?= =?UTF-8?q?=E2=86=92=20Electron=20=E2=86=92=20@jeffrey=20=E2=86=92=20mint)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tezos/series/: generate KidLisp source for feedback/clock/melody sub-series, push each into the logged-in production Electron webview over CDP so it auto-caches under @jeffrey, capture the $code, ready for keeps.mjs mint. preview-shots.mjs emits live inline-source preview URLs. Co-Authored-By: Claude Opus 4.8 (1M context) --- tezos/series/README.md | 87 +++++++++ tezos/series/bridge.mjs | 260 +++++++++++++++++++++++++ tezos/series/generate.mjs | 345 +++++++++++++++++++++++++++++++++ tezos/series/manifest.json | 100 ++++++++++ tezos/series/preview-shots.mjs | 92 +++++++++ 5 files changed, 884 insertions(+) create mode 100644 tezos/series/README.md create mode 100644 tezos/series/bridge.mjs create mode 100644 tezos/series/generate.mjs create mode 100644 tezos/series/manifest.json create mode 100644 tezos/series/preview-shots.mjs diff --git a/tezos/series/README.md b/tezos/series/README.md new file mode 100644 index 0000000000..ec1525515f --- /dev/null +++ b/tezos/series/README.md @@ -0,0 +1,87 @@ +# keeps series — generative → Electron → @jeffrey → mint + +A pipeline for authoring **KidLisp keeps sub-series** and getting them into the +DB under **@jeffrey**, ready to mint into the live v11 keeps contract +(`KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB`). + +The flow, end to end: + +``` +generate.mjs ──▶ manifest.json ──▶ bridge.mjs ──CDP──▶ Electron app + (KidLisp source) (posts source) (production AC, + logged in as @jeffrey) + │ + $code ◀── kidlisp-code-created + │ + keeps.mjs mint $code ──▶ Tezos +``` + +Why this shape: a piece run in a **logged-in** AC session auto-caches its source +via `/api/store-kidlisp` using that session's auth token (`api.authorize()`), so +the `kidlisp` doc records `user = `. We don't juggle CLI tokens — +we ride the webview session. The Electron app already exposes CDP on `:9222`, so +an external script can drive it with **no app changes**. + +## The three series + +| series | engine | what varies per piece | +|----------|-----------------------------------------------|----------------------------------| +| feedback | accumulating pixel buffer (no per-frame wipe) | seed shape, motion, palette | +| clock | wall-clock second triggers (`Ns` / `Ns...`) | tick behavior, reset, palette | +| melody | `(melody …)` + `amplitude`-reactive form | scale/key/feel/waveform, palette | + +## Run it + +**1. Generate the source set** (writes `manifest.json`): + +```bash +node tezos/series/generate.mjs # 17 pieces +node tezos/series/generate.mjs --print # eyeball the source first +node tezos/series/generate.mjs --seed 7 # re-roll palettes (motion stays); + # already-captured $codes are preserved +``` + +**2. Launch the desktop app on PRODUCTION and log in as @jeffrey** (manual, once): + +```bash +cd ac-electron && npm run start:prod # CDP :9222 is on by default +``` + +At the prompt, confirm the handle shows **@jeffrey** (the bridge refuses to run +anonymously unless you pass `--allow-anon`). + +**3. Run the bridge** — feeds each piece in, captures the `$code`: + +```bash +node tezos/series/bridge.mjs # all uncoded pieces +node tezos/series/bridge.mjs --preview # render each ~1.5s, capture nothing +node tezos/series/bridge.mjs --series clock # one series +node tezos/series/bridge.mjs --force # re-run even if code already set +``` + +Captured `$codes` are written back into `manifest.json` (idempotent — +`store-kidlisp` dedupes by source hash, so re-runs are safe). + +**4. Mint each as a keep** (2.5 XTZ keep fee, into v11): + +```bash +node keeps.mjs mint $ +``` + +## Notes / gotchas + +- **Production, not dev.** The bridge warns if the webview target is a localhost + url — `$codes` must save to the live DB, so use `start:prod`. +- **Audio.** `melody` pieces won't make sound during CDP injection (no user + gesture), so `amplitude` reads ~0 while caching. That's cosmetic — the source + is what's stored; it plays for collectors on tap. +- **Identity.** The bridge reads `window.acUSER` and refuses to proceed if it's + null. It does a loose `/jeffrey/` check and warns if the session looks like + someone else. +- **Sub-series tagging.** These mint into the existing v11 collection. The + `series`/`title` live in `manifest.json`; if we later want the trait on-chain, + add it to the TZIP-21 metadata in `keeps.mjs` mint path. +- **Transport.** CDP target discovery + the `Cdp` client mirror + `ac-electron/testing/cdp-latency.mjs`. Inbound message is handled by + `boot.mjs` (`kidlisp-reload`); the `$code` comes back via + `kidlisp-code-created` (`bios.mjs` `post-to-parent`). diff --git a/tezos/series/bridge.mjs b/tezos/series/bridge.mjs new file mode 100644 index 0000000000..8369a00535 --- /dev/null +++ b/tezos/series/bridge.mjs @@ -0,0 +1,260 @@ +#!/usr/bin/env node +// ───────────────────────────────────────────────────────────────────────── +// tezos/series/bridge.mjs +// +// generator → Electron app (production AC, logged in as @jeffrey) → $codes. +// +// Drives the ALREADY-RUNNING Electron app over the Chrome DevTools Protocol +// (port 9222, which every ac-electron start script enables). For each piece in +// manifest.json it: +// 1. posts { type:"kidlisp-reload", code:, createCode:true } into +// the aesthetic.computer webview (handled by boot.mjs:1728), +// 2. the runtime runs it and — because the webview session is authenticated — +// auto-caches the source under @jeffrey via /api/store-kidlisp, +// 3. the page posts `kidlisp-code-created` back; we capture the $code, +// 4. write it into manifest.json. +// +// No Electron code changes. No CLI token. The DB association rides the +// logged-in webview session. After this, the $codes are ready for +// `node keeps.mjs mint $code`. +// +// PREREQUISITES (manual, once): +// • Launch the desktop app on the PRODUCTION url and log in as @jeffrey: +// cd ac-electron && npm run start:prod +// (or: open "/Applications/Aesthetic Computer.app") +// • Make sure you're logged in (handle shows @jeffrey at the prompt). +// +// Usage: +// node tezos/series/bridge.mjs # all uncoded pieces +// node tezos/series/bridge.mjs --series clock # one series only +// node tezos/series/bridge.mjs --force # re-run even if code exists +// node tezos/series/bridge.mjs --preview # render each, don't capture codes +// node tezos/series/bridge.mjs --port 9222 --timeout 25000 +// node tezos/series/bridge.mjs --allow-anon # don't require a logged-in user +// ───────────────────────────────────────────────────────────────────────── + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const MANIFEST = path.join(__dirname, "manifest.json"); + +// ── args ────────────────────────────────────────────────────────────────── +const argv = process.argv.slice(2); +const getArg = (k, d) => { + const i = argv.indexOf(`--${k}`); + return i >= 0 ? argv[i + 1] : d; +}; +const PORT = parseInt(getArg("port", "9222"), 10); +const TIMEOUT = parseInt(getArg("timeout", "25000"), 10); +const ONLY = getArg("series", null); +const FORCE = argv.includes("--force"); +const PREVIEW = argv.includes("--preview"); +const ALLOW_ANON = argv.includes("--allow-anon"); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +// ── minimal CDP client (same wire pattern as ac-electron/testing/cdp-latency) ─ +class Cdp { + constructor(ws) { + this.ws = ws; + this.id = 1; + this.pending = new Map(); + ws.addEventListener("message", (m) => { + const msg = JSON.parse(m.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + msg.error ? reject(new Error(`${msg.error.code}: ${msg.error.message}`)) : resolve(msg.result); + } + }); + } + static async connect(url) { + const ws = new WebSocket(url); + await new Promise((res, rej) => { + ws.addEventListener("open", res, { once: true }); + ws.addEventListener("error", rej, { once: true }); + }); + return new Cdp(ws); + } + send(method, params = {}) { + const id = this.id++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.ws.send(JSON.stringify({ id, method, params })); + }); + } + // Evaluate an expression in the page; await promises, return the value. + async evalJs(expression) { + const r = await this.send("Runtime.evaluate", { + expression, + awaitPromise: true, + returnByValue: true, + }); + if (r.exceptionDetails) { + throw new Error(r.exceptionDetails.exception?.description || r.exceptionDetails.text || "eval error"); + } + return r.result?.value; + } + close() { this.ws.close(); } +} + +async function listTargets(port) { + const r = await fetch(`http://127.0.0.1:${port}/json`); + if (!r.ok) throw new Error(`CDP /json → ${r.status}. Is the app running with --remote-debugging-port=${port}?`); + return r.json(); +} + +// AC content runs inside a (type "webview"); the outer window is a +// file:// "page". Prefer production aesthetic.computer; warn if only localhost. +function findAcTarget(targets) { + const isAc = (t) => /aesthetic\.computer/.test(t.url || "") && (t.type === "webview" || t.type === "page"); + const prod = targets.find((t) => isAc(t) && !/localhost|127\.0\.0\.1/.test(t.url || "")); + return prod || targets.find(isAc) || null; +} + +// ── load manifest ─────────────────────────────────────────────────────────── +if (!fs.existsSync(MANIFEST)) { + console.error(`❌ ${MANIFEST} missing — run: node tezos/series/generate.mjs`); + process.exit(1); +} +const manifest = JSON.parse(fs.readFileSync(MANIFEST, "utf8")); +const seriesNames = ONLY ? [ONLY] : Object.keys(manifest.series); +for (const n of seriesNames) { + if (!manifest.series[n]) { + console.error(`❌ unknown series "${n}". have: ${Object.keys(manifest.series).join(", ")}`); + process.exit(1); + } +} + +const saveManifest = () => fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2)); + +// ── main ────────────────────────────────────────────────────────────────── +async function main() { + console.log(`🔌 CDP :${PORT} …`); + let targets; + try { + targets = await listTargets(PORT); + } catch (e) { + console.error(`❌ ${e.message}`); + console.error(` Launch it first: cd ac-electron && npm run start:prod`); + process.exit(1); + } + const target = findAcTarget(targets); + if (!target) { + console.error(`❌ No aesthetic.computer webview found on :${PORT}.`); + console.error(` Open the app and make sure an AC pane (e.g. /prompt) is showing.`); + process.exit(1); + } + if (/localhost|127\.0\.0\.1/.test(target.url)) { + console.warn(`⚠️ Target is a LOCAL dev url (${target.url}). You asked for production —`); + console.warn(` relaunch with npm run start:prod so $codes save to the live DB.`); + } + console.log(`→ attaching: ${target.url}`); + const cdp = await Cdp.connect(target.webSocketDebuggerUrl); + await cdp.send("Runtime.enable"); + await cdp.send("Page.enable").catch(() => {}); + + // Wait for the runtime bridge (acSEND) to be live. + const bootDeadline = Date.now() + 15000; + let ready = false; + while (Date.now() < bootDeadline) { + const ok = await cdp.evalJs(`typeof window.acSEND === 'function'`).catch(() => false); + if (ok) { ready = true; break; } + await sleep(400); + } + if (!ready) { + console.error(`❌ window.acSEND never appeared — is an AC piece actually loaded in the pane?`); + process.exit(1); + } + + // Identity check. + const who = await cdp.evalJs(`JSON.stringify(window.acUSER || null)`).catch(() => null); + const user = who ? JSON.parse(who) : null; + const ident = user ? (user.handle || user.name || user.sub || "?") : null; + if (!user) { + if (!ALLOW_ANON) { + console.error(`❌ Not logged in (window.acUSER is null). Pieces would cache anonymously.`); + console.error(` Log in as @jeffrey in the app, or pass --allow-anon to override.`); + process.exit(1); + } + console.warn(`⚠️ Not logged in — caching ANONYMOUSLY (--allow-anon).`); + } else { + console.log(`👤 logged in as: ${ident}`); + if (!/jeffrey/i.test(JSON.stringify(user)) && !ALLOW_ANON) { + console.warn(`⚠️ Session does not look like @jeffrey — continuing anyway.`); + } + } + + // Per-piece: inject kidlisp-reload + createCode, await kidlisp-code-created. + const runPiece = async (source) => { + const expr = `new Promise((resolve) => { + const SRC = ${JSON.stringify(source)}; + let done = false; + const onMsg = (e) => { + const d = e && e.data; if (!d) return; + if (d.type === 'kidlisp-code-created' || d.type === 'setCode') { + if (done) return; done = true; + window.removeEventListener('message', onMsg); + resolve(d.code || d.value || null); + } + }; + window.addEventListener('message', onMsg); + window.postMessage({ type: 'kidlisp-reload', code: SRC, createCode: ${PREVIEW ? "false" : "true"} }, '*'); + setTimeout(() => { if (!done){ done = true; window.removeEventListener('message', onMsg); resolve(null); } }, ${TIMEOUT}); + })`; + return cdp.evalJs(expr); + }; + + let ran = 0, got = 0, failed = 0; + for (const name of seriesNames) { + const list = manifest.series[name]; + console.log(`\n══ ${name} (${list.length}) ══`); + for (const it of list) { + if (it.code && !FORCE && !PREVIEW) { + console.log(` • ${name}/${it.title} ↩︎ already $${it.code}`); + continue; + } + process.stdout.write(` • ${name}/${it.title} … `); + try { + const code = await runPiece(it.source); + ran++; + if (PREVIEW) { + console.log(`rendered (preview)`); + await sleep(1500); // linger so you can eyeball it + } else if (code) { + it.code = code; + got++; + saveManifest(); + console.log(`$${code} ✅`); + await sleep(900); + } else { + failed++; + console.log(`no code returned ⚠️ (rendered, but createCode message not seen)`); + await sleep(500); + } + } catch (e) { + failed++; + console.log(`error: ${e.message} ❌`); + } + } + } + + cdp.close(); + console.log(`\n──────── done ────────`); + console.log(`ran ${ran} · captured ${got} · issues ${failed}`); + if (!PREVIEW && got) { + console.log(`\nCaptured $codes:`); + for (const name of seriesNames) { + for (const it of manifest.series[name]) { + if (it.code) console.log(` ${name}/${it.title.padEnd(10)} $${it.code} https://aesthetic.computer/$${it.code}`); + } + } + console.log(`\nMint as keeps (per piece): node keeps.mjs mint $`); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/tezos/series/generate.mjs b/tezos/series/generate.mjs new file mode 100644 index 0000000000..c357c224b0 --- /dev/null +++ b/tezos/series/generate.mjs @@ -0,0 +1,345 @@ +#!/usr/bin/env node +// ───────────────────────────────────────────────────────────────────────── +// tezos/series/generate.mjs +// +// Generates KidLisp source for three keeps SUB-SERIES that mint into the live +// v11 contract (KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB): +// +// • feedback — living textures: a seed shape + accumulating pixel feedback +// (no per-frame wipe → trails compound forever) +// • clock — real-time pieces driven by wall-clock second triggers +// (Ns / Ns... fire on the second; the work visibly ticks) +// • melody — tiny audiovisual songs: (melody ...) + amplitude-reactive form +// +// Output → tezos/series/manifest.json (source + a `code` slot the bridge fills +// after running each piece through the logged-in Electron webview). +// +// Syntax here is grounded in real .lisp fixtures (complex-timing, fia-birthday, +// justsound, lab, brush) and the melody-parser — not guessed. +// +// Usage: +// node tezos/series/generate.mjs # write manifest.json +// node tezos/series/generate.mjs --seed 7 # re-roll palettes (motion stays) +// node tezos/series/generate.mjs --print # print sources, don't write +// ───────────────────────────────────────────────────────────────────────── + +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const MANIFEST = path.join(__dirname, "manifest.json"); + +// ── args ────────────────────────────────────────────────────────────────── +const argv = process.argv.slice(2); +const getArg = (k, d) => { + const i = argv.indexOf(`--${k}`); + return i >= 0 ? argv[i + 1] : d; +}; +const seed = parseInt(getArg("seed", "1"), 10) || 1; +const printOnly = argv.includes("--print"); + +// ── tiny seeded RNG so palette re-rolls are reproducible ──────────────────── +function mulberry32(a) { + return function () { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} +const rng = mulberry32(seed * 2654435761); +const pick = (arr) => arr[Math.floor(rng() * arr.length)]; + +// Curated palettes (each a cycling set used inside (Ns... ...)). All names are +// CSS colors KidLisp accepts. +const PALETTES = [ + ["cyan", "teal", "blue", "navy"], + ["magenta", "purple", "indigo", "blue"], + ["white", "aqua", "skyblue"], + ["pink", "red", "orange", "yellow"], + ["lime", "green", "white"], + ["red", "orange", "yellow", "lime", "green"], + ["gold", "orange", "crimson"], + ["aquamarine", "turquoise", "mediumblue"], + ["violet", "deeppink", "white"], + ["springgreen", "lime", "yellow"], +]; +const cyc = (speed, palette) => `(${speed}s... ${palette.join(" ")})`; + +// ───────────────────────────────────────────────────────────────────────── +// FEEDBACK — accumulating-buffer organisms (no per-frame wipe) +// ───────────────────────────────────────────────────────────────────────── +function feedbackSeries() { + const p = () => pick(PALETTES); + const cs = () => pick(["0.1", "0.15", "0.2", "0.3"]); + + const items = [ + { + title: "drift", + source: [ + "; feedback · drift", + "(once (wipe black))", + `(ink ${cyc(cs(), p())})`, + "(circle (+ width/2 (* 40 (sin))) height/2 4)", + "(scroll 2 0)", + "(zoom 1.003)", + "(blur 1)", + ], + }, + { + title: "vortex", + source: [ + "; feedback · vortex", + "(once (wipe black))", + `(ink ${cyc(cs(), p())})`, + "(box (- width/2 3) (- height/2 3) 6 6)", + "(spin 2)", + "(zoom 1.02)", + "(blur 1)", + ], + }, + { + title: "swell", + source: [ + "; feedback · swell", + "(once (wipe navy))", + `(ink ${cyc(cs(), p())})`, + "(circle width/2 height/2 (+ 6 (* 5 (sin))))", + "(zoom 1.012)", + "(scroll 0 1)", + "(blur 1)", + ], + }, + { + title: "bloom", + source: [ + "; feedback · bloom", + "(once (wipe black))", + `(ink ${cyc(cs(), p())})`, + "(circle (+ width/2 (* 30 (sin))) height/2 5)", + "(zoom 1.01)", + "(spin 1)", + "(blur 2)", + ], + }, + { + title: "weave", + source: [ + "; feedback · weave", + "(once (wipe black))", + `(ink ${cyc(cs(), p())})`, + "(box (+ width/2 (* 50 (sin))) (- height/2 2) 4 4)", + "(scroll 1 1)", + "(zoom 1.004)", + "(blur 1)", + ], + }, + { + title: "ember", + source: [ + "; feedback · ember", + "(once (wipe black))", + `(ink ${cyc(cs(), p())})`, + "(circle width/2 (+ height/2 (* 40 (sin))) 3)", + "(scroll 0 -1)", + "(zoom 0.997)", + "(blur 1)", + ], + }, + ]; + return items.map((i) => ({ ...i, source: i.source.join("\n") })); +} + +// ───────────────────────────────────────────────────────────────────────── +// CLOCK — real-time pieces that visibly tick on wall-clock seconds +// ───────────────────────────────────────────────────────────────────────── +function clockSeries() { + const p = () => pick(PALETTES); + + const items = [ + { + title: "tick", + source: [ + "; clock · tick", + "(wipe black)", + `(ink ${cyc("1", p())})`, + "(circle width/2 height/2 (+ 10 (* 8 (sin))))", + "(ink white)", + "(1s (box 0 (- height 10) width 10))", + ], + }, + { + title: "metronome", + source: [ + "; clock · metronome", + "(0.5s (wipe black))", + `(ink ${cyc("1", p())})`, + "(box (1s... 8 (- width 24)) (- height/2 8) 16 16)", + ], + }, + { + title: "pulse", + source: [ + "; clock · pulse", + "(wipe (1s... black navy))", + `(ink ${cyc("0.5", p())})`, + "(circle width/2 height/2 (+ 6 (* 20 (sin))))", + ], + }, + { + title: "grow", + source: [ + "; clock · grow", + "(8s (wipe black))", + `(ink ${cyc("0.5", p())})`, + "(circle width/2 height/2 (* 6 (mod frame 80)))", + "(blur 1)", + ], + }, + { + title: "pendulum", + source: [ + "; clock · pendulum", + "(wipe black)", + `(ink ${cyc("1", p())})`, + "(line width/2 0 (+ width/2 (* 60 (sin))) height)", + "(ink white)", + "(0.5s (circle (+ width/2 (* 60 (sin))) (- height 8) 5))", + ], + }, + ]; + return items.map((i) => ({ ...i, source: i.source.join("\n") })); +} + +// ───────────────────────────────────────────────────────────────────────── +// MELODY — tiny audiovisual songs ((melody ...) + amplitude-reactive form) +// ───────────────────────────────────────────────────────────────────────── +function melodySeries() { + const p = () => pick(PALETTES); + + const items = [ + { + title: "ascend", + source: [ + "; melody · ascend", + "(wipe black)", + '(melody "4cdefgab5c" 132)', + `(ink ${cyc("0.1", p())})`, + "(circle width/2 height/2 (+ 8 (* 50 amplitude)))", + "(blur 1)", + ], + }, + { + title: "waltz", + source: [ + "; melody · waltz", + "(wipe (0.5s... midnightblue black))", + '(melody "5ceg5ceg 4c.." 120 "3/4" "waltz")', + `(ink ${cyc("0.2", p())})`, + "(circle width/2 height/2 (+ 6 (* 45 amplitude)))", + ], + }, + { + title: "penta", + source: [ + "; melody · penta", + "(wipe black)", + '(melody "5cdega6c" 144 "4/4" "swing")', + `(ink ${cyc("0.15", p())})`, + "(circle width/2 height/2 (+ 6 (* 45 amplitude)))", + "(spin 1)", + ], + }, + { + title: "duet", + source: [ + "; melody · duet", + "(wipe black)", + '(melody "5ceg 3c4g" 120)', + `(ink ${cyc("0.1", p())})`, + "(circle (- width/2 30) height/2 (+ 6 (* 40 amplitude)))", + "(circle (+ width/2 30) height/2 (+ 6 (* 40 amplitude)))", + ], + }, + { + title: "bells", + source: [ + "; melody · bells", + "(wipe midnightblue)", + '(melody "{sine}6cegb6ce" 160)', + `(ink ${cyc("0.1", p())})`, + "(circle width/2 (+ height/2 (* 30 (sin))) (+ 4 (* 30 amplitude)))", + ], + }, + { + title: "drone", + source: [ + "; melody · drone", + "(wipe black)", + '(melody "{sine}3c,,,3g,,," 60)', + `(ink ${cyc("0.3", p())})`, + "(box (- width/2 20) (- height/2 20) (+ 8 (* 60 amplitude)) (+ 8 (* 60 amplitude)))", + "(blur 2)", + ], + }, + ]; + return items.map((i) => ({ ...i, source: i.source.join("\n") })); +} + +// ── assemble ──────────────────────────────────────────────────────────────── +const series = { + feedback: feedbackSeries(), + clock: clockSeries(), + melody: melodySeries(), +}; + +// Preserve any codes already captured by the bridge across re-generation, +// keyed by source text (so re-rolling palettes doesn't wipe minted codes). +let prevBySource = new Map(); +if (fs.existsSync(MANIFEST)) { + try { + const prev = JSON.parse(fs.readFileSync(MANIFEST, "utf8")); + for (const list of Object.values(prev.series || {})) { + for (const it of list) if (it.code) prevBySource.set(it.source, it.code); + } + } catch {} +} + +let total = 0; +for (const list of Object.values(series)) { + for (const it of list) { + it.code = prevBySource.get(it.source) || null; + total++; + } +} + +const manifest = { + contract: "KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB", + network: "mainnet", + seed, + generatedAt: null, // stamped by the caller / bridge; kept null for determinism + note: "Sub-series tagged by `series`. Run tezos/series/bridge.mjs to mint-cache as @jeffrey.", + series, +}; + +if (printOnly) { + for (const [name, list] of Object.entries(series)) { + console.log(`\n══════════ ${name} (${list.length}) ══════════`); + for (const it of list) { + console.log(`\n── ${name}/${it.title} ──`); + console.log(it.source); + } + } +} else { + fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2)); + console.log(`✅ Wrote ${MANIFEST}`); + console.log( + ` ${total} pieces — feedback:${series.feedback.length} clock:${series.clock.length} melody:${series.melody.length} (seed ${seed})`, + ); + const carried = [...prevBySource.values()].length; + if (carried) console.log(` carried ${carried} already-minted $code(s) forward`); + console.log(`\nNext: launch the Electron app (production, logged in as @jeffrey), then:`); + console.log(` node tezos/series/bridge.mjs # run + cache all pieces`); +} diff --git a/tezos/series/manifest.json b/tezos/series/manifest.json new file mode 100644 index 0000000000..f84813f5a3 --- /dev/null +++ b/tezos/series/manifest.json @@ -0,0 +1,100 @@ +{ + "contract": "KT1Q1irsjSZ7EfUN4qHzAB2t7xLBPsAWYwBB", + "network": "mainnet", + "seed": 1, + "generatedAt": null, + "note": "Sub-series tagged by `series`. Run tezos/series/bridge.mjs to mint-cache as @jeffrey.", + "series": { + "feedback": [ + { + "title": "drift", + "source": "; feedback · drift\n(once (wipe black))\n(ink (0.2s... lime green white))\n(circle (+ width/2 (* 40 (sin))) height/2 4)\n(scroll 2 0)\n(zoom 1.003)\n(blur 1)", + "code": null + }, + { + "title": "vortex", + "source": "; feedback · vortex\n(once (wipe black))\n(ink (0.1s... red orange yellow lime green))\n(box (- width/2 3) (- height/2 3) 6 6)\n(spin 2)\n(zoom 1.02)\n(blur 1)", + "code": null + }, + { + "title": "swell", + "source": "; feedback · swell\n(once (wipe navy))\n(ink (0.3s... aquamarine turquoise mediumblue))\n(circle width/2 height/2 (+ 6 (* 5 (sin))))\n(zoom 1.012)\n(scroll 0 1)\n(blur 1)", + "code": null + }, + { + "title": "bloom", + "source": "; feedback · bloom\n(once (wipe black))\n(ink (0.15s... lime green white))\n(circle (+ width/2 (* 30 (sin))) height/2 5)\n(zoom 1.01)\n(spin 1)\n(blur 2)", + "code": null + }, + { + "title": "weave", + "source": "; feedback · weave\n(once (wipe black))\n(ink (0.2s... lime green white))\n(box (+ width/2 (* 50 (sin))) (- height/2 2) 4 4)\n(scroll 1 1)\n(zoom 1.004)\n(blur 1)", + "code": null + }, + { + "title": "ember", + "source": "; feedback · ember\n(once (wipe black))\n(ink (0.1s... red orange yellow lime green))\n(circle width/2 (+ height/2 (* 40 (sin))) 3)\n(scroll 0 -1)\n(zoom 0.997)\n(blur 1)", + "code": null + } + ], + "clock": [ + { + "title": "tick", + "source": "; clock · tick\n(wipe black)\n(ink (1s... lime green white))\n(circle width/2 height/2 (+ 10 (* 8 (sin))))\n(ink white)\n(1s (box 0 (- height 10) width 10))", + "code": null + }, + { + "title": "metronome", + "source": "; clock · metronome\n(0.5s (wipe black))\n(ink (1s... red orange yellow lime green))\n(box (1s... 8 (- width 24)) (- height/2 8) 16 16)", + "code": null + }, + { + "title": "pulse", + "source": "; clock · pulse\n(wipe (1s... black navy))\n(ink (0.5s... springgreen lime yellow))\n(circle width/2 height/2 (+ 6 (* 20 (sin))))", + "code": null + }, + { + "title": "grow", + "source": "; clock · grow\n(8s (wipe black))\n(ink (0.5s... violet deeppink white))\n(circle width/2 height/2 (* 6 (mod frame 80)))\n(blur 1)", + "code": null + }, + { + "title": "pendulum", + "source": "; clock · pendulum\n(wipe black)\n(ink (1s... lime green white))\n(line width/2 0 (+ width/2 (* 60 (sin))) height)\n(ink white)\n(0.5s (circle (+ width/2 (* 60 (sin))) (- height 8) 5))", + "code": null + } + ], + "melody": [ + { + "title": "ascend", + "source": "; melody · ascend\n(wipe black)\n(melody \"4cdefgab5c\" 132)\n(ink (0.1s... springgreen lime yellow))\n(circle width/2 height/2 (+ 8 (* 50 amplitude)))\n(blur 1)", + "code": null + }, + { + "title": "waltz", + "source": "; melody · waltz\n(wipe (0.5s... midnightblue black))\n(melody \"5ceg5ceg 4c..\" 120 \"3/4\" \"waltz\")\n(ink (0.2s... red orange yellow lime green))\n(circle width/2 height/2 (+ 6 (* 45 amplitude)))", + "code": null + }, + { + "title": "penta", + "source": "; melody · penta\n(wipe black)\n(melody \"5cdega6c\" 144 \"4/4\" \"swing\")\n(ink (0.15s... gold orange crimson))\n(circle width/2 height/2 (+ 6 (* 45 amplitude)))\n(spin 1)", + "code": null + }, + { + "title": "duet", + "source": "; melody · duet\n(wipe black)\n(melody \"5ceg 3c4g\" 120)\n(ink (0.1s... aquamarine turquoise mediumblue))\n(circle (- width/2 30) height/2 (+ 6 (* 40 amplitude)))\n(circle (+ width/2 30) height/2 (+ 6 (* 40 amplitude)))", + "code": null + }, + { + "title": "bells", + "source": "; melody · bells\n(wipe midnightblue)\n(melody \"{sine}6cegb6ce\" 160)\n(ink (0.1s... white aqua skyblue))\n(circle width/2 (+ height/2 (* 30 (sin))) (+ 4 (* 30 amplitude)))", + "code": null + }, + { + "title": "drone", + "source": "; melody · drone\n(wipe black)\n(melody \"{sine}3c,,,3g,,,\" 60)\n(ink (0.3s... cyan teal blue navy))\n(box (- width/2 20) (- height/2 20) (+ 8 (* 60 amplitude)) (+ 8 (* 60 amplitude)))\n(blur 2)", + "code": null + } + ] + } +} \ No newline at end of file diff --git a/tezos/series/preview-shots.mjs b/tezos/series/preview-shots.mjs new file mode 100644 index 0000000000..0cfd762d8c --- /dev/null +++ b/tezos/series/preview-shots.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +// tezos/series/preview-shots.mjs +// +// Preview the series WITHOUT the Electron app or login: every piece runs on +// production AC via an inline-source URL. Prints clickable links (live, +// animated) and headlessly screenshots each (via chrome-shot) so we can verify +// each one actually renders before minting. +// +// Usage: +// node tezos/series/preview-shots.mjs # links + stills for all +// node tezos/series/preview-shots.mjs --series clock +// node tezos/series/preview-shots.mjs --no-shots # just print URLs +// node tezos/series/preview-shots.mjs --one feedback/drift # single piece + +import fs from "fs"; +import path from "path"; +import { spawnSync } from "child_process"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.resolve(__dirname, "../.."); +const MANIFEST = path.join(__dirname, "manifest.json"); +const THUMBS = path.join(__dirname, "thumbs"); +const CHROME_SHOT = path.join(REPO, "toolchain/macos/chrome-shot.mjs"); + +const argv = process.argv.slice(2); +const getArg = (k, d) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; }; +const ONLY = getArg("series", null); +const ONE = getArg("one", null); +const noShots = argv.includes("--no-shots"); + +// Inline copy of encodeKidlispForUrl (kidlisp.mjs:15041) — avoids importing the +// giant browser module into node. Keep in sync if the encoder changes. +function encodeKidlispForUrl(source) { + return source + .replace(/ /g, "_") + .replace(/\n/g, "§") + .replace(/%/g, "¤") + .replace(/;/g, "¨") + .replace(/#/g, "%23"); +} + +// Comment lines (the `; series · title` marker) are noise in a preview URL — +// strip them so the link is clean and starts at the first real expression. +// The minted source keeps its comment; this only affects the preview link. +function stripForUrl(source) { + return source + .split("\n") + .filter((l) => l.trim() && !l.trim().startsWith(";")) + .join("\n"); +} + +const manifest = JSON.parse(fs.readFileSync(MANIFEST, "utf8")); +if (!noShots) fs.mkdirSync(THUMBS, { recursive: true }); + +const pieces = []; +for (const [name, list] of Object.entries(manifest.series)) { + if (ONLY && name !== ONLY) continue; + for (const it of list) { + if (ONE && `${name}/${it.title}` !== ONE) continue; + pieces.push({ name, ...it }); + } +} + +console.log(`\n🔗 Live preview links (production — click to run, animated):\n`); +for (const p of pieces) { + const enc = encodeKidlispForUrl(stripForUrl(p.source)); + p.url = `https://aesthetic.computer/${enc}`; + console.log(` ${`${p.name}/${p.title}`.padEnd(18)} ${p.url}`); +} + +if (noShots) process.exit(0); + +console.log(`\n📸 Rendering stills (headless, sequential) → ${path.relative(REPO, THUMBS)}/\n`); +const results = []; +for (const p of pieces) { + const out = path.join(THUMBS, `${p.name}-${p.title}.png`); + const r = spawnSync("node", [ + CHROME_SHOT, `${p.url}?nogap=true`, out, + "--size", "480x480", "--budget", "6000", "--wait", "40000", + ], { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" }); + let ok = r.status === 0 && fs.existsSync(out); + let bytes = ok ? fs.statSync(out).size : 0; + if (ok && bytes < 800) ok = false; // suspiciously tiny = blank/error + results.push({ id: `${p.name}/${p.title}`, ok, bytes, out }); + console.log(` ${ok ? "✅" : "❌"} ${`${p.name}/${p.title}`.padEnd(18)} ${ok ? (bytes + "b") : ("status " + r.status)}`); +} + +const okN = results.filter((r) => r.ok).length; +console.log(`\n${okN}/${results.length} rendered. Stills in ${path.relative(REPO, THUMBS)}/`); +const bad = results.filter((r) => !r.ok); +if (bad.length) console.log(`Needs a look: ${bad.map((b) => b.id).join(", ")}`); -- 2.54.0