Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menuband: instrument-card pipeline (FLUX → IG-square mnemonic deck)

Generates 1080² Pokemon-card-style images for memorising the GM 128
program numbers — anchored to Apple's gs_instruments.dls (the SoundFont
Menu Band actually plays). Each card: huge program number as headline,
researched colored-pencil close-up of a defining detail of the
instrument, "enter menuband and type N to play <name>…" caption, and a
QR to aesthetic.computer/menuband.

Pipeline: bin/instrument-cards.mjs drives the free NVIDIA flux.1-schnell
endpoint with per-instrument researched beats; bin/instrument-card-compose.py
composites typography (YWFT Processing Bold) + QR + family-color chrome
over the full-bleed render. Raw FLUX renders are cached so re-composing
is free. Includes 5-card test set (piano, nylon guitar, violin, trumpet,
flute) covering five GM families.

+654
slab/menuband/assets/instrument-cards/.raw/000-acoustic-grand-piano.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/.raw/024-acoustic-guitar-nylon.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/.raw/040-violin.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/.raw/056-trumpet.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/.raw/073-flute.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/000-acoustic-grand-piano.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/024-acoustic-guitar-nylon.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/040-violin.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/056-trumpet.png

This is a binary file and will not be displayed.

slab/menuband/assets/instrument-cards/073-flute.png

This is a binary file and will not be displayed.

+277
slab/menuband/bin/instrument-card-compose.py
··· 1 + #!/usr/bin/env python3 2 + """instrument-card-compose.py — compose a 1080×1080 Menu Band instrument card. 3 + 4 + The illustration is full-bleed; all chrome overlays it as translucent 5 + gradients with luma-aware drop shadows. 6 + 7 + ┌─── family-color outer frame ───┐ 8 + │ MENU BAND │ ← translucent header gradient 9 + │ │ (family color, fades down) 10 + │ 2 4 │ ← MASSIVE program number 11 + │ ── │ 12 + │ ACOUSTIC GUITAR (NYLON) │ 13 + │ │ 14 + │ full-bleed illustration │ 15 + │ │ 16 + │ │ 17 + │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← translucent footer gradient 18 + │ enter menuband and type 24 │ (dark, fades up) 19 + │ to play acoustic guitar (nylon)… [QR] │ 20 + │ aesthetic.computer/menuband │ 21 + └────────────────────────────────┘ 22 + """ 23 + 24 + from __future__ import annotations 25 + 26 + import argparse 27 + from pathlib import Path 28 + 29 + import qrcode 30 + from PIL import Image, ImageDraw, ImageFont 31 + 32 + 33 + CARD = 1080 34 + PAD = 36 35 + 36 + # ── color helpers ───────────────────────────────────────────────────────── 37 + 38 + 39 + def rgb(s: str) -> tuple[int, int, int]: 40 + return tuple(int(x) for x in s.split(",")) 41 + 42 + 43 + def lighten(c, amt=0.85): 44 + return tuple(int(round(v + (255 - v) * amt)) for v in c) 45 + 46 + 47 + def darken(c, amt=0.3): 48 + return tuple(int(round(v * (1 - amt))) for v in c) 49 + 50 + 51 + # ── typography ──────────────────────────────────────────────────────────── 52 + 53 + 54 + def fit_font(path, text, max_width, max_size, min_size=12): 55 + size = max_size 56 + while size > min_size: 57 + f = ImageFont.truetype(path, size) 58 + bbox = f.getbbox(text) 59 + if (bbox[2] - bbox[0]) <= max_width: 60 + return f 61 + size -= 2 62 + return ImageFont.truetype(path, min_size) 63 + 64 + 65 + def text_with_shadow(layer, xy, text, font, fill, shadow_fill, anchor="la", 66 + shadow_offset=(4, 4), shadow_blur=False): 67 + draw = ImageDraw.Draw(layer) 68 + sx, sy = shadow_offset 69 + draw.text((xy[0] + sx, xy[1] + sy), text, font=font, fill=shadow_fill, anchor=anchor) 70 + draw.text(xy, text, font=font, fill=fill, anchor=anchor) 71 + 72 + 73 + # ── illustration framing ───────────────────────────────────────────────── 74 + 75 + 76 + def fit_illustration_full(raw_path: Path, target: int) -> Image.Image: 77 + img = Image.open(raw_path).convert("RGB") 78 + iw, ih = img.size 79 + scale = max(target / iw, target / ih) 80 + new = (int(iw * scale), int(ih * scale)) 81 + img = img.resize(new, Image.LANCZOS) 82 + nx, ny = new 83 + return img.crop(( 84 + (nx - target) // 2, 85 + (ny - target) // 2, 86 + (nx + target) // 2, 87 + (ny + target) // 2, 88 + )) 89 + 90 + 91 + # ── gradients ──────────────────────────────────────────────────────────── 92 + 93 + 94 + def linear_alpha_band(width, height, start_alpha, end_alpha): 95 + """Create a single-channel L-mode mask, alpha varying top-to-bottom.""" 96 + mask = Image.new("L", (width, height), 0) 97 + pixels = mask.load() 98 + for y in range(height): 99 + t = y / max(1, height - 1) 100 + a = int(round(start_alpha + (end_alpha - start_alpha) * t)) 101 + for x in range(width): 102 + pixels[x, y] = a 103 + return mask 104 + 105 + 106 + def overlay_color_band(card, rect, color, start_alpha, end_alpha): 107 + """Composite a vertical-gradient color band onto card (RGB).""" 108 + x0, y0, x1, y1 = rect 109 + w, h = x1 - x0, y1 - y0 110 + band = Image.new("RGB", (w, h), color) 111 + mask = linear_alpha_band(w, h, start_alpha, end_alpha) 112 + base = card.crop(rect).convert("RGB") 113 + blended = Image.composite(band, base, mask) 114 + card.paste(blended, (x0, y0)) 115 + 116 + 117 + # ── QR ─────────────────────────────────────────────────────────────────── 118 + 119 + 120 + def make_qr(target_url: str, size: int, fg=(20, 20, 24), bg=(255, 255, 255)) -> Image.Image: 121 + qr = qrcode.QRCode( 122 + version=None, 123 + error_correction=qrcode.constants.ERROR_CORRECT_M, 124 + box_size=10, 125 + border=2, 126 + ) 127 + qr.add_data(target_url) 128 + qr.make(fit=True) 129 + return qr.make_image(fill_color=fg, back_color=bg).convert("RGB").resize( 130 + (size, size), Image.NEAREST 131 + ) 132 + 133 + 134 + # ── composer ───────────────────────────────────────────────────────────── 135 + 136 + 137 + def compose(args): 138 + fam_rgb = rgb(args.family_rgb) 139 + fam_dark = darken(fam_rgb, 0.55) 140 + fam_light = lighten(fam_rgb, 0.86) 141 + white = (252, 248, 240) 142 + near_black = (16, 14, 18) 143 + 144 + program_num = int(args.num) 145 + num_str = str(program_num) # no leading zeros, per request 146 + 147 + # ── 1. full-bleed illustration ─────────────────────────────────────── 148 + card = fit_illustration_full(Path(args.raw), CARD).convert("RGB") 149 + draw = ImageDraw.Draw(card) 150 + 151 + # ── 2. translucent header gradient (family color, fades down) ──────── 152 + header_h = 360 153 + overlay_color_band( 154 + card, (0, 0, CARD, header_h), fam_dark, 155 + start_alpha=190, end_alpha=0, 156 + ) 157 + 158 + # ── 3. translucent footer gradient (dark, fades up) ────────────────── 159 + footer_h = 280 160 + overlay_color_band( 161 + card, (0, CARD - footer_h, CARD, CARD), (8, 6, 14), 162 + start_alpha=0, end_alpha=210, 163 + ) 164 + 165 + # ── 4. brand pill top-left ─────────────────────────────────────────── 166 + brand_font = ImageFont.truetype(args.font_bold, 26) 167 + brand_text = "MENU BAND" 168 + bb = brand_font.getbbox(brand_text) 169 + bw = bb[2] - bb[0] + 26 170 + bh = bb[3] - bb[1] + 16 171 + bx = PAD 172 + by = 28 173 + pill = Image.new("RGBA", (bw, bh), fam_rgb + (235,)) 174 + pdraw = ImageDraw.Draw(pill) 175 + pdraw.text((bw / 2, bh / 2), brand_text, font=brand_font, fill=white, anchor="mm") 176 + card.paste(pill, (bx, by), pill) 177 + 178 + # ── 5. MASSIVE program number, white with family-dark shadow ───────── 179 + num_max_w = CARD - 2 * PAD 180 + num_font = fit_font(args.font_bold, num_str, num_max_w, 380, min_size=160) 181 + nb = num_font.getbbox(num_str) 182 + num_w = nb[2] - nb[0] 183 + num_h = nb[3] - nb[1] 184 + num_x = (CARD - num_w) // 2 - nb[0] 185 + num_y = 96 186 + # heavy reverse-color drop shadow for legibility against any background 187 + for dx, dy, alpha in [(8, 8, 200), (5, 5, 230)]: 188 + shadow = Image.new("RGBA", card.size, (0, 0, 0, 0)) 189 + sdraw = ImageDraw.Draw(shadow) 190 + sdraw.text( 191 + (num_x + dx, num_y + dy), 192 + num_str, 193 + font=num_font, 194 + fill=fam_dark + (alpha,), 195 + ) 196 + card.paste(shadow, (0, 0), shadow) 197 + draw.text((num_x, num_y), num_str, font=num_font, fill=white) 198 + 199 + # ── 6. instrument name centered under number ───────────────────────── 200 + name_upper = args.name.upper() 201 + name_max_w = CARD - 2 * (PAD + 30) 202 + name_font = fit_font(args.font_bold, name_upper, name_max_w, 56, min_size=24) 203 + nmb = name_font.getbbox(name_upper) 204 + name_w = nmb[2] - nmb[0] 205 + name_h = nmb[3] - nmb[1] 206 + name_y = num_y + nb[3] + 16 207 + name_x = (CARD - name_w) // 2 - nmb[0] 208 + # dark drop shadow then white fill 209 + for dx, dy in [(3, 3), (2, 2)]: 210 + draw.text((name_x + dx, name_y + dy), name_upper, font=name_font, fill=near_black) 211 + draw.text((name_x, name_y), name_upper, font=name_font, fill=white) 212 + 213 + # ── 7. footer text (left side) ─────────────────────────────────────── 214 + name_lc = args.name.lower() 215 + line1 = f"enter menuband and type {num_str} to play" 216 + line1_font = ImageFont.truetype(args.font_bold, 32) 217 + instr_max_w = CARD - 2 * PAD - 200 # reserve QR area 218 + line2 = f"{name_lc}..." 219 + line2_font = fit_font(args.font_bold, line2, instr_max_w, 50, min_size=22) 220 + url_font = ImageFont.truetype(args.font_regular, 22) 221 + 222 + fx = PAD 223 + line1_y = CARD - footer_h + 70 224 + line2_y = line1_y + 44 225 + url_y = CARD - 50 226 + 227 + for txt, fnt, y, fill in [ 228 + (line1, line1_font, line1_y, (240, 236, 230)), 229 + (line2, line2_font, line2_y, fam_light), 230 + (args.url, url_font, url_y, white), 231 + ]: 232 + # subtle drop shadow on translucent footer 233 + draw.text((fx + 2, y + 2), txt, font=fnt, fill=(0, 0, 0), anchor="la") 234 + draw.text((fx, y), txt, font=fnt, fill=fill, anchor="la") 235 + 236 + # ── 8. QR code, bottom-right ───────────────────────────────────────── 237 + qr_size = 168 238 + qr_x = CARD - PAD - qr_size 239 + qr_y = CARD - PAD - qr_size 240 + qr_img = make_qr(args.qr_target, qr_size) 241 + pad_q = 8 242 + draw.rectangle( 243 + [qr_x - pad_q, qr_y - pad_q, qr_x + qr_size + pad_q, qr_y + qr_size + pad_q], 244 + fill=white, 245 + outline=fam_dark, 246 + width=3, 247 + ) 248 + card.paste(qr_img, (qr_x, qr_y)) 249 + 250 + # ── 9. outer family-color frame ────────────────────────────────────── 251 + frame_w = 12 252 + draw.rectangle([0, 0, CARD - 1, CARD - 1], outline=fam_dark, width=frame_w) 253 + 254 + # ── save ───────────────────────────────────────────────────────────── 255 + out = Path(args.out) 256 + out.parent.mkdir(parents=True, exist_ok=True) 257 + card.save(out, "PNG", optimize=True) 258 + 259 + 260 + def main(): 261 + p = argparse.ArgumentParser() 262 + p.add_argument("--raw", required=True) 263 + p.add_argument("--out", required=True) 264 + p.add_argument("--num", required=True) 265 + p.add_argument("--name", required=True) 266 + p.add_argument("--family", required=True) 267 + p.add_argument("--family-rgb", required=True) 268 + p.add_argument("--font-regular", required=True) 269 + p.add_argument("--font-bold", required=True) 270 + p.add_argument("--url", required=True) 271 + p.add_argument("--qr-target", required=True) 272 + args = p.parse_args() 273 + compose(args) 274 + 275 + 276 + if __name__ == "__main__": 277 + main()
+377
slab/menuband/bin/instrument-cards.mjs
··· 1 + #!/usr/bin/env node 2 + // instrument-cards.mjs — generate Pokemon-card-style 1080×1080 Instagram 3 + // squares for menuband General-MIDI instruments. 4 + // 5 + // Pipeline: 6 + // 1. FLUX (NVIDIA flux.1-schnell, free) renders a 1024×1024 personification 7 + // illustration for the given instrument number. 8 + // 2. Python compositor lays in Processing YWFT typography, the program 9 + // number, the family badge, the QR code → aesthetic.computer/menuband, 10 + // and the printed URL. 11 + // 12 + // Usage: 13 + // node bin/instrument-cards.mjs # default: 5-card test set 14 + // node bin/instrument-cards.mjs --programs 0,24,40,56,73 15 + // node bin/instrument-cards.mjs --programs 0 # single 16 + // node bin/instrument-cards.mjs --out ~/Desktop/menuband-cards 17 + // 18 + // Output: 1080×1080 PNGs named `NNN-<slug>.png` (e.g. `000-acoustic-grand-piano.png`). 19 + 20 + import { spawnSync } from "node:child_process"; 21 + import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; 22 + import { resolve, dirname } from "node:path"; 23 + import { fileURLToPath } from "node:url"; 24 + import { homedir } from "node:os"; 25 + 26 + const __dirname = dirname(fileURLToPath(import.meta.url)); 27 + const REPO = resolve(__dirname, "../../.."); 28 + const POP_PY = `${REPO}/pop/.venv/bin/python`; 29 + const COMPOSER = `${__dirname}/instrument-card-compose.py`; 30 + const FONT_REG = `${REPO}/slab/menuband/Sources/MenuBand/Resources/ywft-processing-regular.ttf`; 31 + const FONT_BOLD = `${REPO}/slab/menuband/Sources/MenuBand/Resources/ywft-processing-bold.ttf`; 32 + 33 + const NVIDIA_KEY = readFileSync( 34 + `${REPO}/aesthetic-computer-vault/.env`, 35 + "utf8", 36 + ).match(/^NVIDIA_API_KEY=(\S+)/m)?.[1]; 37 + if (!NVIDIA_KEY) { 38 + console.error("✗ NVIDIA_API_KEY not found in vault .env"); 39 + process.exit(1); 40 + } 41 + 42 + // ── flags ─────────────────────────────────────────────────────────────── 43 + const flags = {}; 44 + for (let i = 0; i < process.argv.length; i++) { 45 + const a = process.argv[i]; 46 + if (a.startsWith("--")) flags[a.slice(2)] = process.argv[i + 1]; 47 + } 48 + const DEFAULT_TEST = [0, 24, 40, 56, 73]; // piano, nylon guitar, violin, trumpet, flute 49 + const programs = flags.programs 50 + ? flags.programs.split(",").map((s) => parseInt(s, 10)).filter((n) => n >= 0 && n < 128) 51 + : DEFAULT_TEST; 52 + const OUT = flags.out 53 + ? resolve(process.cwd(), flags.out.replace(/^~/, homedir())) 54 + : `${REPO}/slab/menuband/assets/instrument-cards`; 55 + const RAW = `${OUT}/.raw`; 56 + mkdirSync(OUT, { recursive: true }); 57 + mkdirSync(RAW, { recursive: true }); 58 + 59 + // ── GM 128 (mirrors slab/menuband/Sources/MenuBand/GeneralMIDI.swift) ─── 60 + const GM = [ 61 + "Acoustic Grand Piano", "Bright Acoustic Piano", "Electric Grand Piano", "Honky-tonk Piano", 62 + "Electric Piano 1", "Electric Piano 2", "Harpsichord", "Clavinet", 63 + "Celesta", "Glockenspiel", "Music Box", "Vibraphone", 64 + "Marimba", "Xylophone", "Tubular Bells", "Dulcimer", 65 + "Drawbar Organ", "Percussive Organ", "Rock Organ", "Church Organ", 66 + "Reed Organ", "Accordion", "Harmonica", "Tango Accordion", 67 + "Acoustic Guitar (nylon)", "Acoustic Guitar (steel)", "Electric Guitar (jazz)", "Electric Guitar (clean)", 68 + "Electric Guitar (muted)", "Overdriven Guitar", "Distortion Guitar", "Guitar Harmonics", 69 + "Acoustic Bass", "Electric Bass (finger)", "Electric Bass (pick)", "Fretless Bass", 70 + "Slap Bass 1", "Slap Bass 2", "Synth Bass 1", "Synth Bass 2", 71 + "Violin", "Viola", "Cello", "Contrabass", 72 + "Tremolo Strings", "Pizzicato Strings", "Orchestral Harp", "Timpani", 73 + "String Ensemble 1", "String Ensemble 2", "Synth Strings 1", "Synth Strings 2", 74 + "Choir Aahs", "Voice Oohs", "Synth Choir", "Orchestra Hit", 75 + "Trumpet", "Trombone", "Tuba", "Muted Trumpet", 76 + "French Horn", "Brass Section", "Synth Brass 1", "Synth Brass 2", 77 + "Soprano Sax", "Alto Sax", "Tenor Sax", "Baritone Sax", 78 + "Oboe", "English Horn", "Bassoon", "Clarinet", 79 + "Piccolo", "Flute", "Recorder", "Pan Flute", 80 + "Blown Bottle", "Shakuhachi", "Whistle", "Ocarina", 81 + "Lead 1 (square)", "Lead 2 (sawtooth)", "Lead 3 (calliope)", "Lead 4 (chiff)", 82 + "Lead 5 (charang)", "Lead 6 (voice)", "Lead 7 (fifths)", "Lead 8 (bass + lead)", 83 + "Pad 1 (new age)", "Pad 2 (warm)", "Pad 3 (polysynth)", "Pad 4 (choir)", 84 + "Pad 5 (bowed)", "Pad 6 (metallic)", "Pad 7 (halo)", "Pad 8 (sweep)", 85 + "FX 1 (rain)", "FX 2 (soundtrack)", "FX 3 (crystal)", "FX 4 (atmosphere)", 86 + "FX 5 (brightness)", "FX 6 (goblins)", "FX 7 (echoes)", "FX 8 (sci-fi)", 87 + "Sitar", "Banjo", "Shamisen", "Koto", 88 + "Kalimba", "Bagpipe", "Fiddle", "Shanai", 89 + "Tinkle Bell", "Agogo", "Steel Drums", "Woodblock", 90 + "Taiko Drum", "Melodic Tom", "Synth Drum", "Reverse Cymbal", 91 + "Guitar Fret Noise", "Breath Noise", "Seashore", "Bird Tweet", 92 + "Telephone Ring", "Helicopter", "Applause", "Gunshot", 93 + ]; 94 + 95 + const FAMILIES = [ 96 + ["Piano", 0, 7, [255, 90, 110]], 97 + ["Chromatic", 8, 15, [255, 200, 60]], 98 + ["Organ", 16, 23, [180, 90, 220]], 99 + ["Guitar", 24, 31, [240, 130, 50]], 100 + ["Bass", 32, 39, [120, 80, 200]], 101 + ["Strings", 40, 47, [220, 60, 70]], 102 + ["Ensemble", 48, 55, [200, 110, 200]], 103 + ["Brass", 56, 63, [240, 170, 40]], 104 + ["Reed", 64, 71, [70, 170, 110]], 105 + ["Pipe", 72, 79, [80, 180, 220]], 106 + ["Synth Lead", 80, 87, [255, 80, 200]], 107 + ["Synth Pad", 88, 95, [120, 200, 240]], 108 + ["Synth FX", 96, 103,[140, 220, 140]], 109 + ["Ethnic", 104, 111,[210, 130, 80]], 110 + ["Percussive", 112, 119,[230, 230, 80]], 111 + ["Sound FX", 120, 127,[160, 160, 170]], 112 + ]; 113 + 114 + function familyFor(p) { 115 + return FAMILIES.find(([, lo, hi]) => p >= lo && p <= hi); 116 + } 117 + 118 + function slugify(s) { 119 + return s.toLowerCase() 120 + .replace(/[()]/g, "") 121 + .replace(/[^a-z0-9]+/g, "-") 122 + .replace(/^-|-$/g, ""); 123 + } 124 + 125 + // ── illustration prompt ───────────────────────────────────────────────── 126 + // The instrument is the protagonist, rendered IN ITS environment. The scene 127 + // must convey three things: 128 + // sonic vibe — the atmosphere implied by the instrument's sound 129 + // material soul — the physical materials, grain, patina, wear 130 + // legacy — the cultural / historical setting the instrument lives in 131 + // Hockney-style colored-pencil + watercolor wash, hatching for tone. 132 + function promptFor(name, family) { 133 + // Tone clause: positive directives only — FLUX (and its CLIP/T5 encoders) 134 + // misread negations like "no people" and the moderator flags the result. 135 + // 136 + // Aesthetic target: a researched naturalist instrument plate, the kind 137 + // you'd find in a museum specimen catalog or a Diderot encyclopedia, 138 + // executed in layered colored pencil on toned paper. Each beat should 139 + // name a specific historically-correct detail — maker, era, material, 140 + // mechanism — so the illustration reads as accurate, not generic. 141 + const tone = [ 142 + "colored pencil illustration on warm toned paper, naturalist museum-plate style", 143 + "scientific instrument-catalog precision, layered prismacolor strokes with visible pencil hatching", 144 + "soft tapered pencil edges, careful cross-hatching for shadow, paper-tooth texture", 145 + "dramatic close-up macro angle on the defining detail, dynamic perspective", 146 + "warm natural lighting, palpable material textures, historically-correct construction details", 147 + ].join(", "); 148 + 149 + // Per-instrument scene: SETTING + MATERIAL + SONIC MOOD + LEGACY. 150 + const beats = { 151 + "Acoustic Grand Piano": "the action mechanism of a Steinway Model D concert piano in close-up: a row of compressed-wool felt hammers above polished tri-chord steel strings on a cast-iron plate, spruce soundboard visible behind, brass capstans and the escapement of a single key, walnut rim, late-19th-century construction", 152 + "Bright Acoustic Piano": "an upright spinet in a sunlit parlour at golden hour, lace doily on top, sheet music open, mahogany cabinet glowing, lid scratched from years of Sunday afternoons, dust motes in the air", 153 + "Electric Grand Piano": "a tinted-lid Yamaha CP-style stage piano on a 1980s soundstage, chrome stand, soft purple gels, anodised aluminium frame, a coiled cable, lush reverberant studio air", 154 + "Honky-tonk Piano": "a slightly out-of-tune saloon upright in a wooden-floored Western dance hall at night, beer-ringed top, candlelight, missing ivory chips, the thumping joy of a Friday-night singalong", 155 + "Electric Piano 1": "a Fender Rhodes Mark I on a wooden floor in a dim 1970s studio, tine bars visible through an open lid, suitcase legs, soft bell-like air, a Leslie cabinet glowing in the background", 156 + "Electric Piano 2": "a Wurlitzer 200A on a school-music-room stand, cream tolex worn at the corners, red-sparkle keys, a school chalkboard behind, the bittersweet warmth of an old auditorium", 157 + "Harpsichord": "an ornate French double-manual harpsichord in a baroque salon, gilt rocaille flourishes, painted soundboard with flowers and birds, jacks and quills visible through the raised lid, candle sconces and tapestries", 158 + "Clavinet": "a Hohner D6 clavinet under stage lights of a 1970s funk gig, hammered-string mechanism through the smoked plexi top, a coiled cable, talcum-soft purple haze", 159 + "Celesta": "a small upright celesta in a Victorian conservatory, felt-covered hammers striking metal bars, frosted-glass windows, the chime of a snowfall scene", 160 + "Glockenspiel": "a portable orchestral glockenspiel on a rosewood stand backstage, polished steel bars in chromatic order, a pair of brass mallets resting, the bright steely shimmer of a fanfare", 161 + "Music Box": "an antique cylinder music box open on a mahogany side table, brass pin-cylinder and steel comb visible, faded velvet lining, lace doily, the tiny crystalline lullaby of a wound spring", 162 + "Vibraphone": "a Deagan vibraphone in a smoky jazz club, motor-driven discs spinning under the silver bars, brass damper bar, sustain pedal, mid-century chrome stand, the cool slow shimmer of bowed sustain", 163 + "Marimba": "a 5-octave concert marimba in a recital hall, deep rosewood bars over fibreglass resonators, four mallets balanced on the bars, parquet stage, the warm woody resonance of a low roll", 164 + "Xylophone": "a 1920s vaudeville xylophone on a tubular stand, hard rosewood bars, ribbon-wrapped mallets, vaudeville theatre curtains behind, the bright pop of a ragtime run", 165 + "Tubular Bells": "a set of tubular bells in a cathedral nave, brass tubes hanging in chromatic order, a felt mallet on a stand, stained-glass light, the sustained sacred peal of a slow strike", 166 + "Dulcimer": "an Appalachian mountain dulcimer on a quilt-draped chair on a porch, fretted soundboard, walnut and spruce, hand-whittled noter, distant blue-ridge mountains", 167 + "Drawbar Organ": "a Hammond B-3 with Leslie 122 cabinet on stage, drawbars pulled in classic jazz registration, a worn pedalboard, the Leslie horn rotating in slow motion, the warm churchy whirl of a held chord", 168 + "Percussive Organ": "a Hammond B-3 with percussion tab engaged in a Sunday-morning jazz combo, click of the second-harmonic decay, oak woodwork, gospel-blue stage light", 169 + "Rock Organ": "a Hammond C-3 cranked through an overdriven Leslie at a 1970s arena, motors blurred, distortion in the air, sweat on the wood", 170 + "Church Organ": "a vast pipe organ in a Romanesque cathedral, ranks of metal and wood pipes rising into vaulted stone, a console with stops and pedalboard, dust-shaft sunlight, the immense low pedal of a held tone", 171 + "Reed Organ": "a 19th-century Estey parlor reed organ in a New England farmhouse, treadle pedals, knee-swell, hand-stenciled cabinet, lace curtains, the wheezy hymn of a Sunday morning", 172 + "Accordion": "a piano accordion in a Parisian café at twilight, mother-of-pearl keys, mirrored bellows fully open, leather straps, a marble-topped table, the bittersweet musette of a chromatic run", 173 + "Harmonica": "a chromatic harmonica on a worn wooden bartop in a delta blues juke joint, brass reed plates visible through a cracked side, neon beer sign, the crying bend of a held note", 174 + "Tango Accordion": "a bandoneón open on a Buenos Aires café table at midnight, square bellows fully extended, button rows on each end, a glass of red wine, the heart-stopping melancholy of a tango", 175 + "Acoustic Guitar (nylon)":"close-up of the rosette of an Antonio de Torres Jurado 1860s Spanish classical guitar, mother-of-pearl mosaic in concentric rings around the soundhole, fan-braced spruce top, three nylon trebles and three silver-wound nylon basses crossing the soundhole, ebony fingerboard edge, slotted cedar headstock blurred behind", 176 + "Acoustic Guitar (steel)":"a dreadnought steel-string guitar on a porch swing at sunset, satin spruce top with bronze strings glinting, a leather strap and capo, fields beyond, the open-tuned ring of an Americana ballad", 177 + "Electric Guitar (jazz)": "a hollow-body archtop on a brass stand in a smoky 1950s jazz club, sunburst lacquer, f-holes, a single jazz humbucker, a martini on a piano, the warm round tone of a chord-melody", 178 + "Electric Guitar (clean)":"a Fender Stratocaster on a wooden floor of a sunny rehearsal room, three single-coil pickups, maple neck and fingerboard, a small tube amp, the chimey clean shimmer of an arpeggio", 179 + "Electric Guitar (muted)":"a Telecaster with palm-mute hand pose suggested, on a stage in a tight funk band, single-coil bridge pickup, ash body, the percussive chk-chk of a sixteenth-note chop", 180 + "Overdriven Guitar": "a Gibson SG plugged into a cranked Marshall stack, valves glowing, smoke curling from the cone, the saturated growl of a held bend", 181 + "Distortion Guitar": "a black Les Paul on a metal-band stage, scooped-mid distortion in the air, stack of 4×12 cabinets, sweat and stage fog, the chunky palm-muted thrash of a power chord", 182 + "Guitar Harmonics": "a steel-string guitar on a quiet wooden floor, light pinching a 12th-fret harmonic, golden afternoon light, the bell-like ring of a natural overtone", 183 + "Acoustic Bass": "an upright double bass leaning on a piano in a 1940s jazz club, deep cherry-amber varnish, gut strings, a worn bow on the chair, the walking pulse of a slow blues", 184 + "Electric Bass (finger)": "a Fender Precision bass against a tweed amp on a club stage, sunburst body, rosewood fingerboard, fingerstyle calluses on the strings, the rounded thump of a Motown groove", 185 + "Electric Bass (pick)": "a Fender Jazz bass with a tortoiseshell pick on the strings, in a punk-rock garage, the bright clack of a downpicked riff", 186 + "Fretless Bass": "a fretless Jaco-style Jazz bass on a music stand, smooth ebony fingerboard with no fret lines, the singing mwah of a glissando", 187 + "Slap Bass 1": "a Music Man StingRay bass under a single warm stage spot, slap-thumb pose suggested above the strings, the percussive pop of a funk groove", 188 + "Slap Bass 2": "a five-string boutique bass on a soulful R&B stage, double-thumbing pose suggested, slick stage fog", 189 + "Synth Bass 1": "a Minimoog Model D on a 1970s synth-bass stage, hands-on-knob lighting, oscillator banks glowing, the fat saw growl of a low note", 190 + "Synth Bass 2": "a Roland TB-303 on a smoky acid-house stage, silver-and-orange face, the resonant squelch of a sliding pattern", 191 + "Violin": "close-up of the f-hole and bridge of a 1715 Stradivari Cremonese violin, two-footed maple bridge cut by hand, four wound strings rising over it, amber spirit-varnish on flamed-maple ribs, the tip of a pernambuco bow with horsehair in soft focus, scroll just out of frame", 192 + "Viola": "a deeper-toned viola on a velvet-lined case, slightly larger than a violin, warmer red varnish, on a Vienna conservatory stand, the rich alto sigh of a slow melody", 193 + "Cello": "a cello standing alone on a wooden recital stage, endpin in the floor, cherry-amber body, a bow leaning against the chair, single warm spotlight, the deep singing voice of a held G string", 194 + "Contrabass": "a giant double bass leaning against a stone wall of an old conservatory, dark spruce top, gut strings, German bow on a stool, the cavernous low rumble of a pizzicato", 195 + "Tremolo Strings": "a string section in a film-scoring studio, bows blurred mid-tremolo, music stands lit from below, the shivering shimmer of suspense", 196 + "Pizzicato Strings": "a chamber string quartet plucking — fingers visible above strings — in a wood-panelled hall, the dry hopping pluck of a baroque dance", 197 + "Orchestral Harp": "a concert pedal harp on a marble floor of an ornate hall, gilded column, 47 strings catching golden light, seven pedals at the base, the cascading glissando of a fairy scene", 198 + "Timpani": "four pedal timpani in a symphony stage half-circle, copper kettles polished to a mirror, calfskin heads, mallets resting, the rolling thunder of a Mahler climax", 199 + "String Ensemble 1": "a full violin section bowing in unison on a film-scoring stage, music stands in formation, the warm lush wash of a Hollywood sustain", 200 + "String Ensemble 2": "a string quintet in a wood-panelled chamber hall, instruments mid-bow, the silken sustained chord of a film cue", 201 + "Synth Strings 1": "a Roland Juno-60 patched for strings on a stage, blue LCD glow, slow filter sweep suggested by light, the buttery analogue ensemble pad of a slow ballad", 202 + "Synth Strings 2": "an ARP Solina String Ensemble on a wooden organ stand, BBD chips inside suggested by a soft chorus halo, the wide swirling polyphonic strings of a 1970s ballad", 203 + "Choir Aahs": "an empty cathedral nave with stained-glass light, choir stalls lit, music stands set, the suspended 'aah' chord still hanging in the stone", 204 + "Voice Oohs": "a vocal-booth diorama with a single condenser mic on a brass stand, pop filter, headphones on a stool, sound-treatment panels, the rounded 'ooh' of a ghost vocal", 205 + "Synth Choir": "a vintage Mellotron M400 with the lid open, tape racks visible, the breathy choir cassettes loaded, the haunting Ligeti-tinged choir of a sci-fi cue", 206 + "Orchestra Hit": "an empty orchestra stage frozen the instant after a unison stab, every chair facing forward, instruments resting mid-impact, the air still ringing", 207 + "Trumpet": "close-up of the three Périnet piston valves of a Bb Vincent Bach Stradivarius 180S trumpet, mother-of-pearl finger buttons, lacquered yellow-brass casings with silver-plated valve caps, water-key spit valve at the leadpipe, bell flare and tuning slide blurred behind, design lineage from 1818", 208 + "Trombone": "a tenor slide trombone in a brass-band rehearsal room, slide fully extended on a stand, F-attachment trigger and slide grease pot beside, the broad confident gliss of a big-band shout", 209 + "Tuba": "a sousaphone-style tuba on a New Orleans street parade at dusk, huge brass bell facing forward, oompah pulse implied, the warm fat low end of a second-line groove", 210 + "Muted Trumpet": "a trumpet with a Harmon mute in a 1950s blue-lit jazz club, muted bell pointing at a single mic, the cool intimate whisper of a Miles solo", 211 + "French Horn": "a double French horn on a velvet stool in a wood-panelled symphony hall, four rotary valves, coiled brass, hand resting in the bell suggested, the noble heroic call of a Wagnerian theme", 212 + "Brass Section": "a five-piece brass section on a Motown studio stage, two trumpets, two trombones, sax, brass bells in formation, the punchy stab of a horn riff", 213 + "Synth Brass 1": "a Yamaha DX7 in a 1980s pop studio, glowing red display, FM brass patch suggested by light, the bright synthetic brass stab of a power ballad", 214 + "Synth Brass 2": "an Oberheim OB-Xa under stage lights, slabs of analogue brass implied, the fat unison synth-brass of a Van Halen riff", 215 + "Soprano Sax": "a straight soprano saxophone on a brass stand in a smooth-jazz studio, slim brass body, pearl keys, the curling reedy clarity of a Coltrane phrase", 216 + "Alto Sax": "an alto saxophone on a velvet-lined case in a smoky 1940s jazz club, brass body and pearl key cups, neck strap, a martini on the bar, the smoky lyrical voice of a bebop solo", 217 + "Tenor Sax": "a tenor saxophone on a wooden floor of a Brooklyn jazz cellar, deep brass body, the broad smoky baritone of a Coltrane sheets-of-sound run", 218 + "Baritone Sax": "a big bari sax on a stand in a big-band stage, low S-curve neck and large bell, the meaty baritone of a swing-era riff", 219 + "Oboe": "a slim grenadilla oboe on a music stand in a Vienna recital hall, double reed clearly visible, silver keys, the plaintive nasal voice of a tuning A", 220 + "English Horn": "a cor anglais resting on a black case beside a music stand, bulbous pear-shaped bell, alto-oboe length, the haunting pastoral mood of a Dvořák largo", 221 + "Bassoon": "a contrabassoon and bassoon side-by-side on stands in an orchestra pit, dark grenadilla wood with silver keys, double reeds, the warm reedy bass voice of an orchestra", 222 + "Clarinet": "a Bb clarinet on an open velvet-lined case, glossy ebony body, silver keys gleaming, in a Mozart-era recital hall with painted ceilings, the liquid singing voice of a slow movement", 223 + "Piccolo": "a tiny silver piccolo on a marching-band drum, half the length of a flute, head joint and three-piece body, the bright piercing trill of a Sousa march", 224 + "Flute": "close-up of the Boehm-system mechanism of a Powell silver concert flute, open-hole French keys with pinned-pad pivots and steel needle springs, precisely chamfered tone-hole edges, embouchure cut into the head joint blurred behind, lineage from Theobald Boehm 1832", 225 + "Recorder": "a wooden alto recorder in a Renaissance music room, ivory-tipped, on a velvet cloth, hand-painted madrigal book open, the breathy pure tone of a consort", 226 + "Pan Flute": "a set of bamboo pan pipes lashed with cord on a stone wall in an Andean village at dusk, snow-capped peaks distant, the airy plaintive whistle of a quechua melody", 227 + "Blown Bottle": "a row of glass bottles filled to different levels on a wooden table, breath-blown across the tops, soft afternoon light, the hollow round whistle of a folk experiment", 228 + "Shakuhachi": "a bamboo shakuhachi on a tatami mat in a Zen temple, root-end bell, four front holes, ink calligraphy scroll behind, the meditative breathy long tone of a honkyoku", 229 + "Whistle": "a tin whistle on a pub bar in Galway, brass mouthpiece, six finger holes in a green-painted body, a half-finished pint, the high lilting reel of a session", 230 + "Ocarina": "a clay sweet-potato ocarina on a moss-covered stone, finger holes glazed, earthy terracotta, sun dappled forest, the round earthen tone of a folk lullaby", 231 + "Lead 1 (square)": "a Roland SH-101 on a synth desk, square-wave waveform glowing on an oscilloscope behind, the buzzy biting square-wave lead of an 80s arcade", 232 + "Lead 2 (sawtooth)": "a Minimoog with sawtooth output to a screen, the bright cutting saw lead of a prog-rock solo", 233 + "Lead 3 (calliope)": "a steam calliope on a vintage carousel under string lights, brass whistles in a row, steam curling, the carnival joy of a fairground waltz", 234 + "Lead 4 (chiff)": "a Yamaha DX7 with a breathy 'chiff' patch suggested, the airy attack-transient lead of an 80s pop track", 235 + "Lead 5 (charang)": "a 1980s synth keyboard in a Latin-pop studio, pearlescent display, the bright wiry charango-like lead of a Madonna track", 236 + "Lead 6 (voice)": "a Roland V-Synth with a voice-formant lead patch suggested, vocal harmonics rendered as a glowing throat-shape on the screen", 237 + "Lead 7 (fifths)": "a parallel-fifths synth lead patch, an 80s synth on a darkened stage, the heroic open lead of a stadium anthem", 238 + "Lead 8 (bass + lead)": "a Roland JX-3P bass-plus-lead split patch, the punchy 80s synth-pop hook", 239 + "Pad 1 (new age)": "a sound-bath circle with a Yamaha DX7 in the centre, soft pastel halos in the air, the slow shimmering new-age pad of a meditation tape", 240 + "Pad 2 (warm)": "a Roland Jupiter-8 panel glowing in a dim studio, slow filter sweep, a soft warm halo of analogue pad in the air", 241 + "Pad 3 (polysynth)": "an Oberheim Matrix-12 on a wooden stand, big polyphonic chord stacks visible as a soft chord-cloud above, the lush polysynth of an 80s ballad", 242 + "Pad 4 (choir)": "a synth-choir patch on a Korg M1, ghostly choir silhouettes hovering, the haunting choir-pad of a Twin Peaks cue", 243 + "Pad 5 (bowed)": "a synth bowed-pad patch, a violin bow drawing across a synth-string image, the slow swelling bowed pad of an ambient track", 244 + "Pad 6 (metallic)": "a Yamaha SY77 with a metallic FM pad patch, slabs of polished metal hovering, the cold metallic pad of a sci-fi score", 245 + "Pad 7 (halo)": "a halo-pad patch suggested by a glowing soft-blue halo over a dark synth, the angelic suspended halo-pad of a film cue", 246 + "Pad 8 (sweep)": "a Roland D-50 with a slow filter sweep visualised as a gradient ribbon, the cinematic swelling sweep-pad of a movie trailer", 247 + "FX 1 (rain)": "a synth-rain patch — a pane of glass with diagonal raindrops streaking, the gentle bell-like rain-fx of a Vangelis track", 248 + "FX 2 (soundtrack)": "an empty cinema, a single screen lit, the orchestral soundtrack-fx pad lingering", 249 + "FX 3 (crystal)": "a synth-crystal patch — refracting glass shards arranged on a black velvet, the bright shimmering crystal-fx of a video-game intro", 250 + "FX 4 (atmosphere)": "an evaporating mist over a still pond at dawn, the soft synth-atmosphere drone of an ambient piece", 251 + "FX 5 (brightness)": "a flash of bright synth light over a darkened stage, the searing brightness-fx swell", 252 + "FX 6 (goblins)": "a goblin-bell patch on a Roland JX-8P suggested by a forest of tiny mischievous bell-shapes glinting in dim light", 253 + "FX 7 (echoes)": "a synth-echoes patch — concentric ripples on a still water surface, the distant echoing fx of a dub track", 254 + "FX 8 (sci-fi)": "a glowing-cabled modular synth at night, neon glow on patch leads, the alien sci-fi sweep of a 1970s soundtrack", 255 + "Sitar": "a sitar on a silk cushion in a North-Indian raga session, gourd resonator, twenty sympathetic strings, ornate carved neck, the buzzing drone and slow alap", 256 + "Banjo": "a 5-string open-back banjo on a porch swing in Appalachia, drumhead and resonator visible, hand-carved tuners, the bright rolling clawhammer of a mountain tune", 257 + "Shamisen": "a tsugaru shamisen on a tatami mat, square skin-covered body, three silk strings, ivory bachi plectrum, sliding-door silhouette, the percussive snapped attack of a folk piece", 258 + "Koto": "a 13-string koto on a low wooden stand in a Japanese tea room, paulownia wood body with movable bridges, ivory finger picks, sliding shoji screens, the cascading plucked phrase of a sakura melody", 259 + "Kalimba": "a thumb piano on a woven mat in a sunlit African courtyard, hand-tuned metal tines on a hardwood resonator board, the gentle chiming patter of a kalimba ostinato", 260 + "Bagpipe": "a Highland bagpipe on a tartan cloth on a Scottish moor, three drone pipes upright, chanter laid across, sheepskin bag in plaid cover, mist rolling across heather, the unmistakable drone of a pibroch", 261 + "Fiddle": "a fiddle on a wooden chair on the porch of a Cape Breton kitchen ceilidh, the lively foot-tapping reel of a céilidh", 262 + "Shanai": "a north-Indian shehnai on a brass tray in a wedding pavilion, conical wooden body with metal bell, double reed, marigolds garlanding, the auspicious bright tone of a wedding processional", 263 + "Tinkle Bell": "a brass altar bell on a marble altar in a quiet shrine, dome shape, wooden striker, incense smoke, the tiny crystalline tinkle of a held strike", 264 + "Agogo": "a pair of agogo bells in a Brazilian samba school courtyard, two cone-shaped iron bells fused, hammered surfaces, polyrhythmic carnival energy", 265 + "Steel Drums": "a tenor steel pan on a Caribbean beach at sunset, hammered concave pan with chromatic note-fields, palm trees, the bright joyful melody of a calypso", 266 + "Woodblock": "a hardwood Chinese temple woodblock on a wooden stand, hollowed body, mallet resting, the hollow knock-knock of a wuyue beat", 267 + "Taiko Drum": "a giant ō-daiko taiko drum on a wooden cradle, rope-tensioned cowhide head, jin-haori robes folded nearby, festival lanterns, the deep thunder of a matsuri rhythm", 268 + "Melodic Tom": "a row of tom-toms on a 1970s rock drum riser, concert toms tuned in a melodic line, polished maple shells, the descending tom fill of a ballad", 269 + "Synth Drum": "a Simmons SDS-V hexagonal electronic drum kit under stage fog, glowing red logos, the descending pewww of an 80s drum hit", 270 + "Reverse Cymbal": "a single Zildjian crash cymbal mid-frame on a stand, motion blur suggesting a reversed swell, the inhaled whoosh of a reverse-cymbal swell", 271 + "Guitar Fret Noise": "a close-up of a hand sliding along a steel-string fretboard, fingertips on wound strings, the squeaky chirp of a position shift", 272 + "Breath Noise": "a close-up of a flute embouchure with breath-spray rendered as soft white particles, the airy hush of a breath-attack", 273 + "Seashore": "an empty beach at low tide, foam fringe on wet sand, gulls in the sky, the rolling shhhh of a wave", 274 + "Bird Tweet": "a tropical aviary, parrots and finches mid-flight, the trilling chorus of dawn", 275 + "Telephone Ring": "a black rotary phone on a 1950s office desk, bell ringing, brass cradle, the brassy double-ring of an old switchboard", 276 + "Helicopter": "a Huey UH-1 in slow motion at sunset, rotors blurred, dust kicking up, the chopping wokka-wokka of the blades", 277 + "Applause": "a packed concert hall standing ovation seen from the stage, hands blurred mid-clap, the wave of applause filling the air", 278 + "Gunshot": "an old western movie poster image of a smoking revolver on a wooden bartop, no people, the cordite curl of a single shot", 279 + }; 280 + const beat = beats[name] || 281 + `the ${name.toLowerCase()} as the protagonist in its natural cultural setting, period-correct details, materials and historical context that evoke its sonic atmosphere`; 282 + 283 + return `${beat}. ${tone}. family: ${family.toLowerCase()} instrument`; 284 + } 285 + 286 + async function flux(prompt, seed) { 287 + const res = await fetch( 288 + "https://ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-schnell", 289 + { 290 + method: "POST", 291 + headers: { 292 + Authorization: `Bearer ${NVIDIA_KEY}`, 293 + "Content-Type": "application/json", 294 + Accept: "application/json", 295 + }, 296 + body: JSON.stringify({ 297 + prompt, 298 + cfg_scale: 0, 299 + width: 1024, 300 + height: 1024, 301 + seed, 302 + steps: 4, 303 + }), 304 + }, 305 + ); 306 + if (!res.ok) throw new Error(`flux ${res.status}: ${await res.text()}`); 307 + const j = await res.json(); 308 + const b64 = 309 + j.artifacts?.[0]?.base64 || 310 + j.image?.replace(/^data:image\/\w+;base64,/, "") || 311 + j.b64_json; 312 + if (!b64) throw new Error("flux: no image"); 313 + return Buffer.from(b64, "base64"); 314 + } 315 + 316 + // ── main ──────────────────────────────────────────────────────────────── 317 + console.log( 318 + `→ menuband instrument cards · ${programs.length} program(s) → ${OUT}`, 319 + ); 320 + 321 + for (let idx = 0; idx < programs.length; idx++) { 322 + const p = programs[idx]; 323 + const name = GM[p]; 324 + const fam = familyFor(p); 325 + const slug = slugify(name); 326 + const num = String(p).padStart(3, "0"); 327 + const rawPath = `${RAW}/${num}-${slug}.png`; 328 + const finalPath = `${OUT}/${num}-${slug}.png`; 329 + 330 + // Generate raw illustration via FLUX (cached on disk). 331 + // Tiny buffers (<8000B) are FLUX's safety-filter placeholder (solid black); 332 + // retry with a bumped seed up to MAX_TRIES. 333 + if (!existsSync(rawPath)) { 334 + console.log(` [${idx + 1}/${programs.length}] FLUX #${num} ${name}`); 335 + const MAX_TRIES = 6; 336 + let buf = null; 337 + for (let t = 0; t < MAX_TRIES; t++) { 338 + try { 339 + const candidate = await flux(promptFor(name, fam[0]), 7000 + p + t * 9973); 340 + if (candidate.length >= 8000) { buf = candidate; break; } 341 + console.warn(` safety-filter placeholder on try ${t + 1}, retrying…`); 342 + } catch (err) { 343 + console.warn(` flux err try ${t + 1}: ${err.message}`); 344 + } 345 + } 346 + if (!buf) { 347 + console.error(` ✗ FLUX failed for ${name} after ${MAX_TRIES} tries`); 348 + continue; 349 + } 350 + writeFileSync(rawPath, buf); 351 + } else { 352 + console.log(` [${idx + 1}/${programs.length}] cached #${num} ${name}`); 353 + } 354 + 355 + // Composite final card. 356 + const args = [ 357 + COMPOSER, 358 + "--raw", rawPath, 359 + "--out", finalPath, 360 + "--num", String(p), 361 + "--name", name, 362 + "--family", fam[0], 363 + "--family-rgb", fam[3].join(","), 364 + "--font-regular", FONT_REG, 365 + "--font-bold", FONT_BOLD, 366 + "--url", "aesthetic.computer/menuband", 367 + "--qr-target", "https://aesthetic.computer/menuband", 368 + ]; 369 + const r = spawnSync(POP_PY, args, { stdio: "inherit" }); 370 + if (r.status !== 0) { 371 + console.error(` ✗ compositor failed for ${name}`); 372 + continue; 373 + } 374 + console.log(` → ${finalPath}`); 375 + } 376 + 377 + console.log(`✓ done — ${OUT}`);