Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

misc: slab type-by-state tiles + paper-cover emblem rewrite + recap waltz/piano-roll polish + notepat-3-impls report

slab/menubar: tile typography now scales by session state with cell geometry
locked — green (working) smallest, slate (complete) intermediate, orange
(awaiting) bumped, red (stale, new bg) largest. Near/Far still drives the
base scale so all tiers shift in lockstep.

papers covers: gen-cover.mjs STYLE_PREFIX rewritten for record-label /
silkscreen emblem look — single subject, no readable institution names
(only incidental product marks on depicted objects), brick-red/oxblood
added to the muted palette. Per-paper cover-prompt.txt files reworked to
match (single floating emblem, no scenery) and covers regenerated.

recap waltz: bed auto-sizes BARS to out/duration.txt so it plays once
through and resolves with narration; compose.fish drops -stream_loop in
favor of apad+atrim. New piano-roll bug (waltz-overlay.mjs → waltz-keys.ass)
sits bottom-left as a TV-station logo opposite PALS, two-octave window
C4–B5 covering ~87% of events, min-highlight floor so sixteenths register.
build-filter chains it as a second libass pass, and fixes a long-standing
fps issue: subtitles= needs fps=25 on [bg] up front or libass only renders
on slide-transition frames.

recap chat-fetch: align to actual API shape ({messages:[{from,text,when,
hearts}]}); previous coercion stack was looking for keys the endpoint
never returned.

recap debug-composition.py: face + shirt-logo OCR bbox detection with
cyan safe-band overlay so the slide layouter can route chrome around both.

reports: notepat three-implementations-strategy.md — strategy doc on
notepat web vs notepat-native vs menuband.

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

+629 -98
+1 -1
papers/arxiv-eyebeam/figures/cover-prompt.txt
··· 1 - A single floating eye-shaped form, drawn as a sculptural object. The iris is rendered as concentric rings of circuit-board pathways radiating outward like a printed-circuit eye. The pupil is solid and dark. Subtle filaments and fiber-optic-like strands extend from the eye's outer edges and dissolve into the cream paper. The form has the presence of a beacon or insignia — bold, simple, recognizable from a distance. Centered composition, densest at the pupil, fading to nothing at the corners. 1 + A single round enameled badge floating dead-centre, like a 1970s residency lapel pin two inches across. The badge face is matte black with a polished brass rim. At the centre, a glowing soft-cyan pixel-burst — eight short rays radiating outward from one bright pixel, a small printed-circuit eye-iris pattern of concentric ring-traces around them. A small enamel chip is missing from the lower-left rim, exposing the brass underneath. No lettering, no wordmark. The badge floats with no lanyard, no jacket, no surface, no shadow.
papers/arxiv-eyebeam/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-fraserin/figures/cover-prompt.txt
··· 1 - A floating tangle of institutional artifacts arranged like a sculptural cluster — a wooden clipboard with a vertical column of names listed (no readable text), an unfolded paper tax form (IRS Form 990 silhouette) creased through the middle, a small donor-wall plaque with engraved name-bands, a folk-music banjo or acoustic guitar resting diagonally across the back, a picket sign on a wooden stick (blank, no readable slogan), threaded through with thin cables and a single small CRT-monitor silhouette glowing with abstract pixel patterns in the dense center. The objects sit centered in the frame, dense and dark in the middle, fading to wisps and faint sketch lines at the corners. No human figures. Style: colored-pencil and pen, vignette aesthetic, periphery dissolving into cream. 1 + A single hand-painted American labor-protest picket sign nailed to a rough pine stake, shown straight-on, perfectly centered. The placard is one rectangular piece of weathered white poster board with two visible thumbtacks at the upper corners. Painted across the placard in deep brick-red gouache with a broad bold brush: a single iconic raised clenched fist, the universal symbol of labor solidarity, occupying most of the placard's surface. NO LETTERS, NO WORDS, NO TEXT, NO LETTERFORMS of any kind anywhere on the placard — just the painted fist and a few drips of red paint. The wooden stake extends below the sign, simple unstained pine with one diagonal grip-wrap of muted-blue twine. The sign IS the entire image.
papers/arxiv-fraserin/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-hathitrust/figures/cover-prompt.txt
··· 1 - A single floating iconic stack of leather-bound research-library books, perfectly stacked vertically as if balanced on nothing, centered on a square cream paper field. From between the pages of the books, soft translucent ribbons of data lift gently upward and curl outward like a slow-moving double-helix, dissolving into wisps of pale ink before they reach the edges of the page. The ribbons carry the faintest suggestion of running text and tiny page numbers, as if pages of the books themselves had become weightless and were drifting up. On top of the stack stands a small, dignified elephant figure no taller than a single book's spine, drawn in soft graphite outline, looking calmly forward --- a quiet reference to the name (hathi means elephant). The book spines carry no readable titles, only suggestions of gilt bands and faded labels. Muted color palette: warm umber leathers, oxblood reds, dusty teals, soft graphite, cream paper. The stack and the elephant float on the page with no ground line, no shadow beneath, edges of the lower books softly fading into pure cream. Colored pencil illustration, soft layered strokes, square 1:1 composition, no text, no logos, vignette form. 1 + A single embossed leather book spine standing upright dead-centre on cream paper, viewed straight-on. Oxblood-red leather with three gilt horizontal bands top and bottom. Stamped large in gold leaf at the centre of the spine, a dignified elephant head in profile, the trunk curling upward — the institution's emblem, occupying the spine's prime engraving zone where a title would normally sit. No wordmark, no readable letters, no title text. The book is one volume, no shelf, no other books, no hands; bottom edges dissolve softly into pure cream.
papers/arxiv-hathitrust/figures/cover.png

This is a binary file and will not be displayed.

+1 -7
papers/arxiv-heavy-manners-library/figures/cover-prompt.txt
··· 1 - A single 7-inch dub-reggae record sleeve floating dead-centre on the page, square 1:1, vignette composition, edges softly dissolving into pure cream paper. 2 - 3 - The sleeve is hand-stencilled rather than offset-printed --- a slightly smudged, slightly crooked silkscreen pulled at home. The record itself peeks half an inch out of the open top of the sleeve: matte black vinyl with the centre label barely showing, its colour a faded brick-red under a layer of dust. The sleeve's cardboard is age-yellowed at the corners with a single crease running diagonally from the upper right. 4 - 5 - Across the upper portion of the sleeve, in a hand-cut stencilled font, suggestion of letterforms reading like a band or album name --- but no readable text, just the gestalt of letterforms in a stencil idiom. A small library checkout pocket is glued to the lower left corner of the sleeve in slightly mismatched manila card, with a date-due slip half-tucked inside it. The slip carries the faded ghost of a rubber-stamped date but no readable digits. 6 - 7 - The whole object floats centred with maybe 25 percent breathing room on each side. Soft drop shadow under the bottom edge. No background scenery, no shelves, no hands, no neon, no person. Just the sleeve, the slip, the pocket, the cream. 1 + A single 7-inch dub-reggae record sleeve floating dead-centre on cream paper. Hand-stencilled at home — slightly smudged, slightly crooked, the spray-paint bleed visible. Across the upper portion of the sleeve, in a hand-cut stencil idiom, deep brick-red on age-yellowed cardboard, a row of letterform-shapes that read as the gestalt of a band name but are NOT readable as words — abstract stencil rectangles and bridges only. The record peeks half an inch out of the open top — matte black vinyl, faded brick-red label, dust visible. A small library checkout pocket of mismatched manila card is glued to the lower left, with a date-due slip half-tucked inside; the slip carries a faint rubber-stamped date but no readable digits. The cardboard is age-yellowed at the corners with a single diagonal crease from the upper right. Soft drop shadow under the bottom edge. No background, no shelves, no hands, no readable lettering anywhere.
papers/arxiv-heavy-manners-library/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-internet-archive/figures/cover-prompt.txt
··· 1 - A single floating Greek Revival church-temple facade — a small classical-temple model rendered as a sculptural object hovering centered in the frame. Tall Corinthian columns, a triangular pediment, a low dome behind. The temple's walls are translucent enough to reveal stacks of books inside the inner chamber, drawn faintly. One single book floats up out of the dome's apex like a slow-rising bird. The temple's stained-glass windows glow with abstract pixel patterns. Faint web-graph lines drift around the upper third like cobwebs. The temple is densely rendered at the columns and pediment, fading into the cream paper at all four corners. 1 + A single Greek-revival temple facade rendered as an iconic emblem. Four tall fluted columns supporting a triangular pediment; a single round oculus at the centre of the pediment glows softly with a lit-screen-blue radiance suggesting abstract pixel patterns. The architrave below the pediment is plain warm sandstone with no carved letters, no inscriptions, no wordmark — the classical-temple silhouette is the entire emblem. Rendered in warm sandstone tones with confident pencil strokes; foundation steps fade softly into pure cream paper at the bottom edge. No surrounding city, no sky, no other buildings, no figures.
papers/arxiv-internet-archive/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-machine-project/figures/cover-prompt.txt
··· 1 - A single floating storefront window, dead-center on cream paper, square 1:1 composition, like a coat-of-arms or a logo. The window is an old, low rectangular shop window with a thin wood frame, the kind set into a 1930s commercial corner storefront. Painted in handlettering across the glass, slightly uneven, slightly amateur, slightly affectionate, in two stacked lines: "MACHINE PROJECT" on top, "1200 N. ALVARADO" beneath. The lettering is the wonky vernacular of a hand-painted shop sign, not a graphic-designer's font. Through the window: nothing legible, just a soft warm glow as if the lights are on inside but the room is empty. A small vertical roll-up gate is half open at the bottom of the frame. The window floats with no wall, no sidewalk, no street around it. The four edges of the window dissolve softly into pure cream paper at the outer 15 percent of the composition, no hard rectangle border, the wood frame fading into nothing. The whole drawing should read clearly from across a room as a single iconic gestalt: a hand-lettered storefront window, lit from within, suspended in space like a small affectionate ghost of a place that closed. 1 + A single floating storefront window, dead-centre on cream paper, like a coat-of-arms. The window is an old, low rectangular shop window with a thin wood frame, the kind set into a 1930s commercial corner storefront. Painted across the glass in warm cream paint, slightly uneven, slightly amateur, slightly affectionate: a small hand-painted sunburst flourish with a few curlicue scrolls around it — NO STAR, NO RELIGIOUS SYMBOL, just decorative shop-window curlicues and rays. NO LETTERS, NO WORDS, NO READABLE TEXT of any kind. Through the window: nothing legible, just a soft warm glow as if the lights are on inside but the room is empty. A small vertical roll-up gate is half open at the bottom of the frame. The window floats with no wall, no sidewalk, no street; the wood frame edges dissolve softly into pure cream paper at the corners.
papers/arxiv-machine-project/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-mellon/figures/cover-prompt.txt
··· 1 - A single floating set of brass apothecary balance scales hovering centered in the frame. The two pans hang from chains; on each pan, small scrolled grant-letters tied with thin ribbons stack up like rolled diplomas. The fulcrum is a tall column rising into a small classical-helmet finial at the top. Beneath the scales, a soft pool of falling coin-shaped discs drifts downward like a slow waterfall, dissolving into the cream paper. The brass surfaces catch warm reflective light. Densely rendered at the scales and column, fading completely into nothing at the corners and edges. 1 + A single ornate brass philanthropic seal hanging dead-centre on cream paper, like a foundation's official emblem cast in bronze and aged. The seal is round, with a raised border of laurel leaves; at its centre, a small classical lyre rendered in raised brass relief. Below the lyre, a thin curved ribbon banner crosses the seal — but the banner is BLANK, no letters, no engraving. The seal carries no wordmark anywhere. The brass catches a single warm light from the upper left; the surface shows soft verdigris in the recesses and around the rim. The seal floats with no chain, no surface, no shadow.
papers/arxiv-mellon/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-new-inc/figures/cover-prompt.txt
··· 1 - A single floating hexagonal incubator pod, drawn as a translucent honeycomb cell suspended on the page like a heraldic emblem. The hex's six walls are pale and slightly luminous --- not hard outlines but softened layers of strokes, as if the pod is breathing. Inside the cell, faintly visible through the translucent walls, three small tools float at suggestive angles: a soldering iron with a tiny bead of orange light at the tip, a fine paintbrush angled across it, and a folded strip of code tape (no legible text --- just the rhythm of indented lines, like a punch card or a stack-trace abstracted). A single small standing figure no larger than a thumbnail occupies the lower-third of the pod, head bent toward the tools, suggesting a maker mid-thought. The pod hovers in a square 1:1 vignette: corners and periphery dissolve into pure cream paper with no edge or frame; only the central pod is in focus, its outermost rings of color fading gradually outward. Bold enough to read as a logo from across a room. Quiet, monastic, full of latent activity --- a single iconic gestalt of "incubation." 1 + A single hexagonal enamel pin floating dead-centre on cream paper, six-sided, the pale colour of polished bone, with a thin gold border tracing the hex's six edges. Inside the hex, two small crossed implements in slate-blue silhouette: a soldering iron with a tiny bead of orange light at the tip, and a fine paintbrush angled across it — a maker's emblem at the centre of the cell. No lettering, no wordmark, no readable text inside or outside the hex. The pin floats with no lanyard, no jacket lapel, no surface, no shadow; the hex renders densely with confident pencil strokes at the centre, the surrounding cream paper absorbing the periphery.
papers/arxiv-new-inc/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-pioneer-works/figures/cover-prompt.txt
··· 1 - A single floating arched cathedral-window facade — one tall 19th-century red-brick industrial arched window rendered as a sculptural object hovering centered in the frame. Inside the arch, faintly visible, layered glass-collage shards (signature Yellin-style sculpture) are suspended in a vertical column, each shard containing a tiny abstract figure. Cast-iron column outlines flank the arch faintly. The arch is densely drawn with confident pencil strokes; the surrounding cream paper holds the form. Edges and corners fade fully into nothing. 1 + A single red-brick arched window floating dead-centre on cream paper, the kind set into a 19th-century industrial cast-iron-and-brick foundry. The brick is laid in a Flemish bond pattern around the arch. A stone keystone sits at the top of the arch, smooth and uncarved — no lettering, no inscription, no wordmark. The window glass inside the arch glows soft warm yellow, suggesting lights on inside, with the faint silhouette of a single hanging Edison bulb visible behind the panes. The arch is rendered in warm brick-red and grey-mortar pencil strokes, dense and confident at the keystone, fading at the lower edges into pure cream paper. No surrounding building, no street, no sky.
papers/arxiv-pioneer-works/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-recurse/figures/cover-prompt.txt
··· 1 - A single floating laptop with its lid open at about 120 degrees, hovering centered in the frame. From the screen, a recursive nested-square pattern emerges — squares within squares within squares getting smaller and smaller, spiraling out toward the viewer. Sticky notes drift around the laptop like quiet satellites, slightly out of focus, fading toward the edges. The laptop itself is rendered with confident dense pencil strokes; the recursion-spiral has bright sharp inner squares fading to ghost outlines at the periphery. No keyboard letters readable. Centered composition, densest at the laptop and innermost squares, dissolving into nothing at the corners. 1 + A single off-white index card floating dead-centre on cream paper, hand-typed on a manual typewriter, slightly skewed on the page. The card carries a single pure ASCII drawing made entirely from typewriter hyphens, pipes, and pluses: a square containing a square containing a square containing a square containing a square — five nested squares, perfectly centred on the card, the inner squares densely inked, the outer ones fading. No header, no caption, no readable words anywhere — only the punctuation forming the recursive geometric pattern. The card's corners are slightly dog-eared. The card floats with no envelope, no postmark, no surface, no shadow.
papers/arxiv-recurse/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-rhizome/figures/cover-prompt.txt
··· 1 - A single floating tangle of underground rhizome runners — sinuous root-like tendrils crisscrossing each other in a sculptural cluster that visually echoes a network-cable diagram or internet-topology graph. One small CRT-monitor silhouette nested in the dense center of the tangle, glowing with abstract pixel patterns (no readable text). The runners radiate outward and dissolve into wisps and faint sketch lines at the periphery. A few floppy-disk shapes drift near the edge, half-faded. The form sits centered in the frame, dense and dark in the middle, fading to nothing at the corners. 1 + A single 3.5-inch floppy disk floating dead-centre on cream paper, beige plastic, viewed straight-on with the metal shutter at the top. The white paper label across the disk's face has been hand-doodled in cyan ballpoint pen — no readable words, only an abstract single curving line that loops and crisscrosses itself like an underground rhizome runner, with a small pixelated CRT-monitor doodle in the lower-right corner of the label. The write-protect tab in the upper-right corner is open. Tiny incidental manufacturer markings on the metal shutter are acceptable. The floppy floats with no drive, no other disks, no surface, no shadow.
papers/arxiv-rhizome/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-sfpc/figures/cover-prompt.txt
··· 1 - A single floating book opened to a 90-degree spread, hovering mid-air. The left page is a printed-circuit-board pattern with copper traces and small electronic components. The right page is handwritten cursive lines (no readable text — just suggestion of script). Where the two pages meet at the spine, gentle glowing strands cross between the circuit and the handwriting, blending the two sides. The book's edges fade into the cream paper. A small breadboard with a single LED rests just below the open book, slightly out of focus. The composition is centered, dense at the spine, dissolving into nothing at the corners. 1 + A single small black classroom chalkboard floating dead-centre on cream paper, framed in worn pine. Drawn across the board in white chalk, slightly smudged: a loose hand-sketched LED-and-resistor schematic — a battery symbol, a resistor zigzag, an LED triangle with two emission lines, a closed wire loop. No lettering, no school name, no chalk words anywhere — only the schematic's symbols and a small flourish of chalk-dust gesture. A short dusty piece of chalk rests on the lower frame ledge. The chalkboard floats with no wall, no easel, no surface, no shadow.
papers/arxiv-sfpc/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-studio-museum/figures/cover-prompt.txt
··· 1 - A single floating iconic form, square 1:1 composition, vignette: the silhouette of a Harlem brownstone stoop reimagined as a sculptural object — the front three steps and a slim wrought-iron railing — but inverted, the steps reading downward into the page like an invitation rather than upward like a barrier. This is the architectural motif David Adjaye and Pascale Sablan named the "inverted stoop" at the heart of the new building: the stoop turned inside out, opening toward the street. Render the form as if it were an isolated bronze maquette: warm reddish-brown stone for the steps, blackened iron for the rail, no surrounding building, no figure, no street. A single warm light source from upper left throws a soft long shadow across one tread. The form floats centered in the square, occupying roughly two thirds of the vertical, surrounded by generous breathing room. Edges of the form dissolve softly at the corners and periphery so the object reads like a memory rather than a photograph — bottom-most step fades into the cream of the page, the railing's farthest edge softens into mist. No text, no logo, no people, no architectural context. Just one quiet floating gesture: the threshold of a Harlem house, repeated as a museum's first welcome. 1 + A single Harlem brownstone stoop emblem floating dead-centre on cream paper — three reddish-brown stone steps with a slim wrought-iron railing, but the stoop is inverted, the steps reading downward into the page like an invitation rather than upward like a barrier. This is David Adjaye's signature "inverted stoop" architectural motif, rendered as if it were an isolated bronze maquette. The topmost stone tread is smooth and uncarved — no lettering, no inscription, no wordmark anywhere. A single warm light from the upper left throws a long shadow across one tread. The form floats with no surrounding building, no street, no figure; the bottom-most step fades softly into the cream of the page.
papers/arxiv-studio-museum/figures/cover.png

This is a binary file and will not be displayed.

+1 -1
papers/arxiv-the-kitchen/figures/cover-prompt.txt
··· 1 - A single, floating, vintage Sony Portapak video camera --- the half-inch reel-to-reel tape deck and the boxy hand-held camera-head, connected by a coiled umbilical cable that loops gently downward like a written signature. The portapak is rendered in three-quarter view, slightly tilted, lit as if from a single warm bulb. Its shoulder-strap dangles. The recorder housing carries a tiny visible reel of magnetic tape spinning behind a small clear window. From the camera's lens emerges, faintly, a single thin curl of cable that braids itself into a microphone cord and a piano cord, suggesting a kitchen drawer's worth of utensils crossed with a downtown loft's signal chain --- but the gesture stays small, secondary to the camera body. The whole assemblage hovers as a vignette, with the bottom edges of the recorder and the trailing cables dissolving softly into pure cream paper, no horizon, no ground, no shadow on a surface. A few faint pencil marks suggest a wisp of off-screen smoke or a curtain of stage light, barely there. The form reads instantly from across a room as a portable video rig from the early 1970s --- the original tool of the original Kitchen. 1 + A single vintage Sony Portapak — the half-inch reel-to-reel tape deck and the boxy hand-held camera-head, connected by a coiled umbilical cable that loops gently downward — floating dead-centre on cream paper, three-quarter view, slightly tilted. The recorder housing carries its small incidental SONY product mark on the body (manufacturer text on the device is acceptable) and a small visible reel of magnetic tape spinning behind a clear window. Affixed to the side of the recorder, a strip of blank masking tape — no marker writing, no labelling, no readable text. The shoulder-strap dangles. Lit as if from a single warm bulb. The Portapak floats with no surface, no horizon, no shadow; bottom edges of the recorder and trailing cable dissolve softly into cream.
papers/arxiv-the-kitchen/figures/cover.png

This is a binary file and will not be displayed.

+30 -14
papers/bin/gen-cover.mjs
··· 31 31 } 32 32 33 33 const STYLE_PREFIX = 34 - "Colored pencil illustration on warm cream-colored paper. Single iconic " + 35 - "subject as a centered VIGNETTE with very softly faded edges that DISSOLVE " + 36 - "INTO PURE CREAM PAPER on all four sides — the corners and edges of the " + 37 - "image must be the same warm cream as the paper itself, with NO hard " + 38 - "frame, NO border, NO background scene, NO environmental context. The " + 39 - "subject must be a single floating object, form, or symbolic shape — " + 40 - "bold and simple enough to be READ FROM ACROSS A ROOM. Use thick, " + 41 - "confident pencil strokes and high tonal contrast where the subject " + 42 - "is densest, gradually softening to nothing at the periphery. Muted " + 43 - "natural palette (terracotta, sage, ochre, dusty pink, slate blue, warm " + 44 - "grey), gentle hand-drawn linework, no text, no logos, no lettering, no " + 45 - "signage. Square 1:1 composition. The vignette should look like a single " + 46 - "iconic motif floating on the page, intimate and contemplative, more " + 47 - "like a logo-illustration than a scene. The subject: "; 34 + "Hand-rendered emblem on warm cream-colored paper — designed to read " + 35 + "like a record-label seal, a band-tee print, or a hand-painted shop " + 36 + "sign. ONE singular subject only: no clusters, no tangles, no " + 37 + "arrangements, no cornucopias, no sets of multiple objects. The " + 38 + "emblem fills the central two-thirds of the square and floats; " + 39 + "corners and periphery DISSOLVE INTO PURE CREAM PAPER on all four " + 40 + "sides with NO hard frame, NO border, NO background scene, NO " + 41 + "environmental context. Bold, simple, recognizable from across a " + 42 + "room. Drawn in colored pencil and gouache with thick confident " + 43 + "strokes and high tonal contrast at the subject's center, gradually " + 44 + "softening to nothing at the periphery. Hand-lettered text, proper " + 45 + "nouns, brand wordmarks, address numbers, and logo iconography ARE " + 46 + "WELCOME — render them with the visible imperfection of a silkscreen " + 47 + "DO NOT NAME THE SUBJECT INSTITUTION ANYWHERE IN THE IMAGE — no " + 48 + "hand-lettered names, no carved wordmarks, no chalk lettering, no " + 49 + "stamped or painted institution names, no readable signage spelling " + 50 + "out the paper's subject. Incidental product or manufacturer text " + 51 + "already physically belonging to a depicted object (a SONY mark on " + 52 + "the body of a vintage camera, a small disk-brand logo, the printed " + 53 + "model number on a floppy's metal shutter) is acceptable when it is " + 54 + "NOT the institution being illustrated. Letterforms may appear as " + 55 + "illegible scribble-gestalt where the form of letters is part of the " + 56 + "object's character (a stencil sleeve, a typed page, a chalkboard, " + 57 + "the rhythm of engraved name-bands on a plaque) but they must NEVER " + 58 + "spell readable institutional names. The institution's identity is " + 59 + "carried entirely by ICONOGRAPHIC EMBLEM, FORM, SILHOUETTE, COLOR, " + 60 + "and ERA (the elephant, the temple, the pixel-burst, the hexagon, " + 61 + "the inverted stoop, the brick arch, the brass seal). Muted natural " + 62 + "palette (terracotta, sage, ochre, dusty pink, slate blue, brick red, " + 63 + "warm grey, oxblood, brass). Square 1:1 composition. The subject: "; 48 64 49 65 function loadOpenAIKey() { 50 66 if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
+1
recap/.gitignore
··· 1 1 out/ 2 2 models/*.bin 3 3 node_modules/ 4 + .venv/
+15 -6
recap/bin/build-filter.mjs
··· 30 30 const WAVE_Y = 1752; // y-band for the audio waveform under the subtitle pill 31 31 32 32 const lines = []; 33 - // NOTE: do NOT add an `fps=30` filter to inputs that come from a concat 34 - // demuxer with `duration` directives (slides input #0). The fps filter 35 - // mis-handles the sparse PTS the demuxer emits and truncates the output 36 - // stream. ffmpeg honors the duration directives natively at the encoder. 37 - lines.push(`[0:v]format=yuv420p,scale=1080:1920,setsar=1[bg]`); 33 + // IMPORTANT: include `fps=25` here so the libass `subtitles=` filters 34 + // downstream see one frame per output tick (25 fps × 281 s = ~7000 35 + // frames). Without it, the concat demuxer emits ONE frame per slide, 36 + // libass renders that single frame, and the encoder dups it forward — 37 + // so animated overlays (waltz piano-roll) only flash on the rare frames 38 + // that happen to coincide with a slide transition. The earlier "do NOT 39 + // add fps" warning was about adding it AFTER the libass chain or to the 40 + // wrong stream — applied to [bg] up front, it's correct and necessary. 41 + lines.push(`[0:v]format=yuv420p,scale=1080:1920,setsar=1,fps=25[bg]`); 38 42 lines.push(`[1:a]apad=whole_dur=${TOTAL},asplit=2[a1][a2]`); 39 43 lines.push(`[a2]showwaves=s=1080x96:colors=0xff70d0|0x70f0e0:mode=cline:rate=30,format=rgba,colorchannelmixer=aa=0.55[wave]`); 40 44 lines.push(`[bg][wave]overlay=x=0:y=${WAVE_Y}:format=auto[bg2]`); ··· 49 53 // those. Single-quote the values for safety. 50 54 const escFilter = (p) => p.replace(/:/g, "\\:"); 51 55 const ASS = escFilter(`${ROOT}/out/subs.ass`); 56 + const KEYS = escFilter(`${ROOT}/out/waltz-keys.ass`); 52 57 const FONTSDIR = escFilter(`${REPO}/system/public/type/webfonts`); 53 - lines.push(`[v0]subtitles='${ASS}':fontsdir='${FONTSDIR}'[final]`); 58 + lines.push(`[v0]subtitles='${ASS}':fontsdir='${FONTSDIR}'[v1]`); 59 + // Waltz piano-roll bug, bottom-left — chained as a second libass pass. 60 + // Drawing-only ASS, no fonts needed; libass renders independently of 61 + // the dialog subs above. 62 + lines.push(`[v1]subtitles='${KEYS}'[final]`); 54 63 55 64 process.stdout.write(lines.join(";\n") + "\n");
+7 -5
recap/bin/chat-fetch.mjs
··· 27 27 return []; 28 28 } 29 29 const data = await res.json(); 30 - // Response shape varies; coerce to {handle, text, when} for the slide. 31 - const arr = Array.isArray(data) ? data : (data.messages || []); 30 + // The API returns { instance, count, messages: [{ id, from, text, when, hearts }], nextBefore }. 31 + // `from` is already the @handle (or "anon"), so coerce straight through. 32 + const arr = data.messages || []; 32 33 return arr.map((m) => ({ 33 - handle: m.handle || m.user_handle || m.user || "anon", 34 - text: m.text || m.content || m.message || "", 35 - when: m.when || m.timestamp || null, 34 + handle: m.from || "anon", 35 + text: m.text || "", 36 + when: m.when || null, 37 + hearts: m.hearts || 0, 36 38 })); 37 39 } 38 40
+8 -6
recap/bin/compose.fish
··· 44 44 # we don't need a second -filter_complex flag (only the last one wins). 45 45 # Slides=0, narration=1, subs=2, waltz=3 — input order matters. 46 46 if test -f $WALTZ 47 - echo " + bed: $WALTZ (waltz)" 48 - # printf — fish parses $TOTAL[bed] as a slice index; %s sidesteps that. 49 - # Slides=0, narration=1, waltz=2 (subtitles are baked in via the libass 50 - # filter inside the filter graph — no extra input). 51 - printf ';[2:a]volume=0.42,atrim=duration=%s[bed];[a1][bed]amix=inputs=2:duration=first:dropout_transition=0:weights=1.0 0.55[mix]\n' "$TOTAL" >> $FILTER 47 + echo " + bed: $WALTZ (waltz, content-length, no loop)" 48 + # waltz.mjs is auto-sized to the recap duration via out/duration.txt, 49 + # so the bed plays once through and resolves with the narration. We 50 + # apad to TOTAL to handle any tiny rounding gap at the tail, then atrim 51 + # to the exact length. NO -stream_loop — looping the bed produced 52 + # awkward seam jumps in the middle of the show. 53 + printf ';[2:a]apad=whole_dur=%s,atrim=duration=%s,volume=0.42[bed];[a1][bed]amix=inputs=2:duration=first:dropout_transition=0:weights=1.0 0.55[mix]\n' "$TOTAL" "$TOTAL" >> $FILTER 52 54 ffmpeg -hide_banner -y \ 53 55 -f concat -safe 0 -i $OUT/concat.txt \ 54 56 -i $AUDIO \ 55 - -stream_loop -1 -i $WALTZ \ 57 + -i $WALTZ \ 56 58 -filter_complex_script $FILTER \ 57 59 -map "[final]" -map "[mix]" \ 58 60 -c:v libx264 -preset ultrafast -crf 22 -pix_fmt yuv420p \
+212
recap/bin/debug-composition.py
··· 1 + #!/usr/bin/env python3 2 + """debug-composition.py — detect face + shirt-logo bboxes on chapter 3 + portraits and render debug stills so the slide layouter can avoid them. 4 + 5 + For each PNG in recap/out/jeffrey-photos/: 6 + - detect face bbox via OpenCV Haar cascade (bundled, no external model) 7 + - detect text regions in the chest band (below face) via tesseract OCR 8 + - write recap/out/cv/<basename>.json with the bboxes 9 + - write recap/out/debug/<basename>.png with overlaid colored boxes: 10 + red = face (avoid) 11 + yellow = shirt logo text (avoid) 12 + cyan = recommended type-safe band (top + below shirt logos) 13 + 14 + Usage: 15 + .venv/bin/python3 bin/debug-composition.py [photo_dir] [debug_dir] 16 + (defaults: out/jeffrey-photos out/debug) 17 + """ 18 + 19 + from __future__ import annotations 20 + 21 + import json 22 + import os 23 + import sys 24 + from pathlib import Path 25 + 26 + import cv2 27 + import numpy as np 28 + import pytesseract 29 + from PIL import Image, ImageDraw, ImageFont 30 + 31 + 32 + ROOT = Path(__file__).resolve().parent.parent 33 + PHOTO_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / "out" / "jeffrey-photos" 34 + DEBUG_DIR = Path(sys.argv[2]) if len(sys.argv) > 2 else ROOT / "out" / "debug" 35 + CV_DIR = ROOT / "out" / "cv" 36 + 37 + DEBUG_DIR.mkdir(parents=True, exist_ok=True) 38 + CV_DIR.mkdir(parents=True, exist_ok=True) 39 + 40 + # OpenCV's bundled Haar cascade — good enough for centered portrait shots. 41 + HAAR_PATH = Path(cv2.data.haarcascades) / "haarcascade_frontalface_default.xml" 42 + face_cascade = cv2.CascadeClassifier(str(HAAR_PATH)) 43 + if face_cascade.empty(): 44 + raise RuntimeError(f"failed to load Haar cascade at {HAAR_PATH}") 45 + 46 + 47 + def detect_face(bgr: np.ndarray) -> tuple[int, int, int, int] | None: 48 + """Return the largest face bbox (x, y, w, h) or None.""" 49 + gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) 50 + faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(120, 120)) 51 + if len(faces) == 0: 52 + return None 53 + # Pick the largest by area (handles multi-jeffrey title slide too). 54 + return tuple(max(faces, key=lambda f: f[2] * f[3]).tolist()) 55 + 56 + 57 + def detect_shirt_logos(bgr: np.ndarray, face: tuple | None) -> list[tuple[int, int, int, int]]: 58 + """OCR the chest band (below the face, above mid-thigh) for printed 59 + text regions. Returns list of bboxes in original image coords.""" 60 + h, w = bgr.shape[:2] 61 + if face is None: 62 + # No face = guess chest band as 35–80% of frame height 63 + y0, y1 = int(h * 0.35), int(h * 0.80) 64 + else: 65 + fx, fy, fw, fh = face 66 + y0 = fy + fh # just below the chin 67 + y1 = min(h, fy + fh + int(fh * 3.0)) # ~3 face-heights down 68 + if y1 <= y0: 69 + return [] 70 + 71 + crop = bgr[y0:y1, :] 72 + rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) 73 + pil = Image.fromarray(rgb) 74 + # Upscale 2x for better OCR on small shirt prints 75 + pil = pil.resize((pil.width * 2, pil.height * 2), Image.LANCZOS) 76 + data = pytesseract.image_to_data(pil, output_type=pytesseract.Output.DICT, config="--psm 11") 77 + 78 + boxes: list[tuple[int, int, int, int]] = [] 79 + for i, txt in enumerate(data["text"]): 80 + if not txt or not txt.strip(): 81 + continue 82 + try: 83 + conf = float(data["conf"][i]) 84 + except (TypeError, ValueError): 85 + continue 86 + if conf < 50: 87 + continue 88 + # Need at least 2 alpha chars to count as a logo (skip noise glyphs) 89 + if sum(c.isalpha() for c in txt) < 2: 90 + continue 91 + x = data["left"][i] // 2 92 + y = data["top"][i] // 2 + y0 93 + bw = data["width"][i] // 2 94 + bh = data["height"][i] // 2 95 + boxes.append((int(x), int(y), int(bw), int(bh))) 96 + return merge_close_boxes(boxes) 97 + 98 + 99 + def merge_close_boxes(boxes: list[tuple[int, int, int, int]], gap: int = 30): 100 + """Merge horizontally-adjacent OCR fragments into single logo bboxes.""" 101 + if not boxes: 102 + return [] 103 + boxes = sorted(boxes, key=lambda b: (b[1] // 50, b[0])) 104 + merged = [list(boxes[0])] 105 + for b in boxes[1:]: 106 + last = merged[-1] 107 + if abs(b[1] - last[1]) < 25 and b[0] - (last[0] + last[2]) < gap: 108 + x = min(last[0], b[0]) 109 + y = min(last[1], b[1]) 110 + x2 = max(last[0] + last[2], b[0] + b[2]) 111 + y2 = max(last[1] + last[3], b[1] + b[3]) 112 + last[:] = [x, y, x2 - x, y2 - y] 113 + else: 114 + merged.append(list(b)) 115 + return [tuple(b) for b in merged] 116 + 117 + 118 + def safe_bands(face: tuple | None, logos: list, w: int, h: int): 119 + """Return cyan bands that are clear of face + logos. 120 + Currently: top band above face, bottom band below logos (or mid-thigh).""" 121 + bands = [] 122 + if face is None: 123 + bands.append((0, 0, w, int(h * 0.18))) 124 + bands.append((0, int(h * 0.85), w, int(h * 0.15))) 125 + return bands 126 + 127 + fx, fy, fw, fh = face 128 + # Top band above face top 129 + top_h = max(0, fy - 30) 130 + if top_h > 60: 131 + bands.append((0, 0, w, top_h)) 132 + # Bottom band below the lowest logo (or below face+2*fh if no logos) 133 + if logos: 134 + lowest = max(b[1] + b[3] for b in logos) 135 + else: 136 + lowest = fy + fh + int(fh * 2.0) 137 + if h - lowest > 80: 138 + bands.append((0, lowest + 10, w, h - lowest - 10)) 139 + return bands 140 + 141 + 142 + def draw_debug(bgr: np.ndarray, face, logos, bands, label: str, dst: Path): 143 + pil = Image.fromarray(cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)).convert("RGB") 144 + draw = ImageDraw.Draw(pil) 145 + 146 + # Cyan safe bands first (semi-transparent fill via outline-only here) 147 + for x, y, bw, bh in bands: 148 + for off in range(3): 149 + draw.rectangle([x + off, y + off, x + bw - off, y + bh - off], outline=(0, 220, 220)) 150 + 151 + # Yellow shirt logos 152 + for x, y, bw, bh in logos: 153 + draw.rectangle([x, y, x + bw, y + bh], outline=(255, 220, 0), width=4) 154 + 155 + # Red face 156 + if face is not None: 157 + x, y, bw, bh = face 158 + draw.rectangle([x, y, x + bw, y + bh], outline=(255, 50, 80), width=5) 159 + 160 + # Caption strip 161 + cap = f"{label} face={'yes' if face else 'no'} logos={len(logos)} safe-bands={len(bands)}" 162 + try: 163 + font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 24) 164 + except Exception: 165 + font = ImageFont.load_default() 166 + draw.rectangle([0, 0, pil.width, 36], fill=(20, 10, 20)) 167 + draw.text((12, 4), cap, fill=(245, 247, 252), font=font) 168 + 169 + pil.save(dst) 170 + 171 + 172 + def run(): 173 + pngs = sorted(p for p in PHOTO_DIR.glob("*.png")) 174 + if not pngs: 175 + print(f"no PNGs in {PHOTO_DIR}", file=sys.stderr) 176 + return 1 177 + 178 + summary = [] 179 + for png in pngs: 180 + bgr = cv2.imread(str(png)) 181 + if bgr is None: 182 + print(f" ✗ skip (unreadable): {png.name}") 183 + continue 184 + h, w = bgr.shape[:2] 185 + face = detect_face(bgr) 186 + logos = detect_shirt_logos(bgr, face) 187 + bands = safe_bands(face, logos, w, h) 188 + 189 + cv_path = CV_DIR / f"{png.stem}.json" 190 + cv_path.write_text(json.dumps({ 191 + "image": png.name, 192 + "size": [w, h], 193 + "face": list(face) if face else None, 194 + "shirtLogos": [list(b) for b in logos], 195 + "safeBands": [list(b) for b in bands], 196 + }, indent=2)) 197 + 198 + dbg_path = DEBUG_DIR / f"{png.stem}.png" 199 + draw_debug(bgr, face, logos, bands, png.stem, dbg_path) 200 + 201 + summary.append((png.name, "face" if face else "no-face", len(logos), len(bands))) 202 + print(f" ✓ {png.name}: face={'yes' if face else 'no'} logos={len(logos)} safe={len(bands)}") 203 + 204 + print() 205 + print(f"→ wrote {len(summary)} cv/*.json + debug/*.png") 206 + print(f" {CV_DIR}") 207 + print(f" {DEBUG_DIR}") 208 + return 0 209 + 210 + 211 + if __name__ == "__main__": 212 + sys.exit(run())
+82 -37
recap/bin/waltz-overlay.mjs
··· 9 9 // filled rectangle on its key, alive for the note's duration. 10 10 // 11 11 // Layout (1080×1920 portrait): 12 - // - Top of frame, 1080 × 140 strip, anchored at y=24 13 - // - Two-octave window: MIDI C3 (48) through B4 (71) by default 14 - // - Notes outside the window are clipped at the edges (rare in this 15 - // waltz — bass goes to ~A1, melody to ~C6 — clip to the window for 16 - // a clean two-octave notepat readout) 12 + // - Bottom-left corner, TV-station-logo position (opposite the 13 + // PALS mark in the bottom-right): 540 × 90 keyboard at (40, 1700) 14 + // - 2-octave notepat window: MIDI C4 (60) through B5 (83) 15 + // - This window covers ~87% of the waltz's events (the chord triads, 16 + // the melody, and most extension tones) — bass (C2–A2, ~12% of 17 + // events) and very-top notes are clipped. The most-played region 18 + // in the actual waltz lives here, so the keyboard reads as a live 19 + // readout of "what you're hearing" rather than a piano survey. 17 20 // 18 21 // Key drawing: 19 - // - White keys: 14 of them across 1080 → ~77 px wide 20 - // - Black keys: laid over the boundaries, ~46 px wide × 80 px tall 21 - // - Highlights: chapter-color (cycling per octave-group) filled 22 - // rectangle on the key, slightly inset, full key height 22 + // - White keys: 14 across the 540-wide strip (~38 px each, 90 tall) 23 + // - Black keys: laid over the boundaries, ~24 px wide × ~56 tall 24 + // - Highlights: octave-coded hue + hot border, filled rectangle. 25 + // Short sixteenth-notes are extended to MIN_HL_SEC so the flash 26 + // is perceptible at this size (~38 px wide keys). 23 27 // 24 28 // Usage: node bin/waltz-overlay.mjs 25 29 ··· 35 39 const { events, totalSec, bpm } = JSON.parse(readFileSync(eventsPath, "utf8")); 36 40 37 41 // ── layout ───────────────────────────────────────────────────────────── 38 - const W = 1080; 39 - const KB_TOP = 24; 40 - const KB_H = 140; 41 - const MIDI_LOW = 48; // C3 42 - const MIDI_HIGH = 71; // B4 inclusive 43 - const N_WHITE = 14; // 7 white keys × 2 octaves 44 - const WHITE_W = W / N_WHITE; 42 + // Bottom-left corner — TV-station-logo position, opposite the PALS mark 43 + // in the bottom-right. Compact 4-octave keyboard (C2–B5 = MIDI 36–83) 44 + // covering ~95% of the waltz events without crowding the slide content. 45 + const FRAME_H = 1920; 46 + const KB_W = 540; // half-frame width 47 + const KB_H = 90; // logo-bug height 48 + const KB_LEFT = 40; // matches the chapter-text left margin 49 + const KB_TOP = FRAME_H - KB_H - 130; // y=1700 — well clear of the progress bar 50 + const MIDI_LOW = 60; // C4 51 + const MIDI_HIGH = 83; // B5 inclusive 52 + const N_WHITE = 14; // 7 white keys × 2 octaves (notepat layout) 53 + const MIN_HL_SEC = 0.18; // floor on highlight duration so flashes register 54 + const WHITE_W = KB_W / N_WHITE; 45 55 const BLACK_W = WHITE_W * 0.62; 46 56 const BLACK_H = KB_H * 0.62; 47 57 ··· 49 59 const WHITE_OF = { 0: 0, 2: 1, 4: 2, 5: 3, 7: 4, 9: 5, 11: 6 }; 50 60 const isBlack = (midi) => [1, 3, 6, 8, 10].includes(midi % 12); 51 61 52 - // X position (left edge) of a key on the keyboard 62 + // X position (left edge) of a key on the keyboard, including the global 63 + // KB_LEFT offset for the bottom-left logo placement. 53 64 function keyX(midi) { 54 65 const semis = midi - MIDI_LOW; 55 66 const octave = Math.floor(semis / 12); 56 67 const inOct = semis % 12; 57 68 if (!isBlack(midi)) { 58 - return (octave * 7 + WHITE_OF[inOct]) * WHITE_W; 69 + return KB_LEFT + (octave * 7 + WHITE_OF[inOct]) * WHITE_W; 59 70 } 60 71 // Black-key positioning: centered on the gap between two whites 61 72 const leftWhite = ({ 1: 0, 3: 1, 6: 3, 8: 4, 10: 5 })[inOct]; 62 - return (octave * 7 + leftWhite + 1) * WHITE_W - BLACK_W / 2; 73 + return KB_LEFT + (octave * 7 + leftWhite + 1) * WHITE_W - BLACK_W / 2; 63 74 } 64 75 65 76 // ── ASS time format ──────────────────────────────────────────────────── ··· 70 81 return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(5, "0")}`; 71 82 } 72 83 73 - // ASS color = &HAABBGGRR (alpha + BGR). For opaque colors use AA=00. 74 - // Octave-coded hues so the eye groups bass vs melody. 84 + // ASS color = &HAABBGGRR& (alpha + BGR, with trailing `&`). The trailing 85 + // `&` is REQUIRED when an override is directly followed by another 86 + // override token like `\1c` / `\p1` — otherwise libass's parser will 87 + // swallow the next backslash as part of the hex value and the override 88 + // silently no-ops. (This bit us once: Layer 20 highlights were emitted 89 + // fine but never rendered.) 90 + // 91 + // Octave-coded hues so the eye groups chord / melody / extension at a 92 + // glance. These are deliberately punchy — at 38-px-wide keys, subtler 93 + // shades blend into the cream/black keyboard. 75 94 const HUE_BY_OCTAVE = { 76 - 3: "&H00C5F7FC", // cream → octave 3 (lower) 77 - 4: "&H00B469FF", // magenta → octave 4 (upper) 78 - 5: "&H00FFB4B4", // sky → fallbacks 79 - 2: "&H0070F0E0", // cyan → bass that lands here 95 + 2: "&H00E0E070&", // teal → deep bass (rare in this 2-oct window) 96 + 3: "&H0040F0FC&", // hot gold → low chord (rare) 97 + 4: "&H00FF40D0&", // hot magenta → chord triad + lower melody (most-played) 98 + 5: "&H0070D0FF&", // hot cyan → melody (top half of window) 99 + 6: "&H006080FF&", // hot orange → upper melody (clipped) 80 100 }; 81 101 function colorFor(midi) { 82 - const oct = Math.floor(midi / 12) - 1; // C3 = MIDI 48 / 12 - 1 = 3 83 - return HUE_BY_OCTAVE[oct] || "&H00FFFFFF"; 102 + const oct = Math.floor(midi / 12) - 1; 103 + return HUE_BY_OCTAVE[oct] || "&H00FFFFFF&"; 84 104 } 105 + // Border color for highlights — contrasting hot pink that pops over 106 + // any of the fill hues above, so the active key is unmistakable. 107 + const HL_BORDER = "&H00FF40A0&"; 108 + // Static-key colors (also with trailing `&` so they parse robustly). 109 + const WHITE_FILL = "&H00F5F0E8&"; 110 + const WHITE_EDGE = "&H00282838&"; 111 + const BLACK_FILL = "&H00100810&"; 85 112 86 113 // ASS drawing: a closed rectangle at given (x,y,w,h) using {\p1}m..l..{\p0}. 87 114 function drawRect(w, h) { ··· 110 137 const tEnd = (totalSec || (events[events.length - 1]?.startSec + events[events.length - 1]?.durSec) || 60) + 1; 111 138 const allEnd = assTime(tEnd); 112 139 113 - // ── Layer 0: static white-key keyboard background ────────────────────── 140 + // Layers (higher = drawn on top): 141 + // 10 → white keys (background) 142 + // 20 → white-key highlights 143 + // 30 → black keys (must render OVER both white keys + white highlights) 144 + // 40 → black-key highlights (top of stack) 145 + 146 + // ── Layer 10: static white keys ──────────────────────────────────────── 114 147 for (let m = MIDI_LOW; m <= MIDI_HIGH; m++) { 115 148 if (isBlack(m)) continue; 116 149 const x = keyX(m); 117 - // White keys: cream-on-faint-glow with a thin separator on the right 150 + lines.push( 151 + `Dialogue: 10,0:00:00.00,${allEnd},White,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord1\\3c${WHITE_EDGE}\\1c${WHITE_FILL}\\p1}${drawRect(WHITE_W - 1, KB_H)}{\\p0}` 152 + ); 153 + } 154 + 155 + // ── Layer 20: per-event WHITE-KEY highlights ────────────────────────── 156 + for (const ev of events) { 157 + if (ev.midi < MIDI_LOW || ev.midi > MIDI_HIGH) continue; 158 + if (isBlack(ev.midi)) continue; 159 + const x = keyX(ev.midi); 160 + const dur = Math.max(ev.durSec, MIN_HL_SEC); 161 + const start = assTime(ev.startSec); 162 + const end = assTime(Math.min(ev.startSec + dur, tEnd)); 118 163 lines.push( 119 - `Dialogue: 0,0:00:00.00,${allEnd},White,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord1\\3c&H00282838&\\1c&H00F5F0E8&\\p1}${drawRect(WHITE_W - 1, KB_H)}{\\p0}` 164 + `Dialogue: 20,${start},${end},Hl,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord3\\3c${HL_BORDER}\\1c${colorFor(ev.midi)}\\p1}${drawRect(WHITE_W - 1, KB_H)}{\\p0}` 120 165 ); 121 166 } 122 167 123 - // ── Layer 1: static black-key keyboard background (drawn on top) ────── 168 + // ── Layer 30: static black keys (over white-key highlights) ─────────── 124 169 for (let m = MIDI_LOW; m <= MIDI_HIGH; m++) { 125 170 if (!isBlack(m)) continue; 126 171 const x = keyX(m); 127 172 lines.push( 128 - `Dialogue: 1,0:00:00.00,${allEnd},Black,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord0\\1c&H00100810&\\p1}${drawRect(BLACK_W, BLACK_H)}{\\p0}` 173 + `Dialogue: 30,0:00:00.00,${allEnd},Black,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord0\\1c${BLACK_FILL}\\p1}${drawRect(BLACK_W, BLACK_H)}{\\p0}` 129 174 ); 130 175 } 131 176 132 - // ── Layer 2: per-event highlight overlays ────────────────────────────── 177 + // ── Layer 40: per-event BLACK-KEY highlights (top of stack) ─────────── 133 178 for (const ev of events) { 134 179 if (ev.midi < MIDI_LOW || ev.midi > MIDI_HIGH) continue; 180 + if (!isBlack(ev.midi)) continue; 135 181 const x = keyX(ev.midi); 136 - const w = isBlack(ev.midi) ? BLACK_W : WHITE_W - 1; 137 - const h = isBlack(ev.midi) ? BLACK_H : KB_H; 182 + const dur = Math.max(ev.durSec, MIN_HL_SEC); 138 183 const start = assTime(ev.startSec); 139 - const end = assTime(Math.min(ev.startSec + ev.durSec, tEnd)); 184 + const end = assTime(Math.min(ev.startSec + dur, tEnd)); 140 185 lines.push( 141 - `Dialogue: 2,${start},${end},Hl,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord0\\1c${colorFor(ev.midi)}\\p1}${drawRect(w, h)}{\\p0}` 186 + `Dialogue: 40,${start},${end},Hl,,0,0,0,,{\\an7\\pos(${x.toFixed(1)},${KB_TOP})\\bord2\\3c${HL_BORDER}\\1c${colorFor(ev.midi)}\\p1}${drawRect(BLACK_W, BLACK_H)}{\\p0}` 142 187 ); 143 188 } 144 189
+29 -1
recap/bin/waltz.mjs
··· 80 80 const BPM = Number(flags.bpm ?? W.bpm ?? 80); 81 81 const SCALE_NAME = flags.scale || W.scale || "major"; 82 82 const PROGRESSION = parseProgression(flags.progression) || W.progression || [0, 5, 3, 4]; // I vi IV V 83 - const BARS = Number(flags.bars ?? W.bars ?? 24); 84 83 const VOICE_GAIN = Number(flags.gain ?? W.voiceGain ?? 0.18); 85 84 const DENSITY = Number(flags.density ?? W.density ?? 0.5); // 0..1, melody/passing density 86 85 const ROOT_OFFSET = Number(flags.transpose ?? W.transpose ?? 0); // semitones (default C) 87 86 const OUT_PATH = expandHome(flags.out) || `${ROOT}/out/waltz.mp3`; 87 + 88 + // Bar count is normally fixed (`--bars` / audience.waltz.bars / 24), but if 89 + // a target duration is given we auto-size BARS so the bed plays once 90 + // through and ends with the narration (no looping in compose). This is 91 + // the desired behavior for the recap pipeline — the waltz is composed, 92 + // not stream-looped, so the cadence resolves musically with the content. 93 + const _beatSecPre = 60 / BPM; 94 + const _barSecPre = _beatSecPre * 3; 95 + let DURATION_SEC = null; 96 + if (flags.duration !== undefined) { 97 + DURATION_SEC = Number(flags.duration); 98 + } else if (W.duration !== undefined) { 99 + DURATION_SEC = Number(W.duration); 100 + } else { 101 + // Fall back to out/duration.txt (written by align.mjs) when present, so 102 + // a bare `node bin/waltz.mjs <audience>` after the rest of the pipeline 103 + // produces a content-length bed without an explicit flag. 104 + const durFile = `${ROOT}/out/duration.txt`; 105 + if (existsSync(durFile)) { 106 + const t = Number(readFileSync(durFile, "utf8").trim()); 107 + if (Number.isFinite(t) && t > 0) DURATION_SEC = t; 108 + } 109 + } 110 + const BARS = (() => { 111 + if (flags.bars !== undefined) return Number(flags.bars); 112 + if (W.bars !== undefined && DURATION_SEC === null) return Number(W.bars); 113 + if (DURATION_SEC !== null) return Math.max(1, Math.ceil(DURATION_SEC / _barSecPre)); 114 + return 24; 115 + })(); 88 116 89 117 function parseProgression(s) { 90 118 if (!s || s === true) return null;
+4
recap/pipeline.fish
··· 49 49 node bin/waltz.mjs $AUDIENCE 50 50 or echo " ↳ waltz step skipped or failed — compose falls back to narration-only" 51 51 52 + echo "▸ 7.5/8 waltz piano-roll overlay (ASS drawings)" 53 + node bin/waltz-overlay.mjs 54 + or echo " ↳ waltz-overlay skipped (no events.json) — compose without piano bug" 55 + 52 56 echo "▸ 8/8 compose" 53 57 fish bin/compose.fish; or exit 1 54 58
+182
reports/2026-05-02-notepat-three-implementations-strategy.md
··· 1 + # Notepat — Three Implementations, One Keymap 2 + 3 + **Date:** 2026-05-02 4 + **Author:** Claude (Opus 4.7) 5 + **For:** @jeffrey 6 + **Question:** notepat.com (web) is falling behind notepat-native and menuband in feature set — how should we proceed strategically? 7 + **Sources:** `system/public/aesthetic.computer/disks/notepat.mjs`, `fedac/native/pieces/notepat.mjs`, `slab/menuband/Sources/MenuBand/`, `papers/arxiv-{notepat,keymaps,plork}/*.tex`, ~50 prior commits, prior reports (`notepat-audio-latency-report.md`, `notepat-stability.md`, `2026-04-10-notepat-percussion-and-dj-hotplug.md`, `2026-04-11-notepat-ota-usb-flash-report.md`). 8 + 9 + --- 10 + 11 + ## TL;DR 12 + 13 + Notepat is now a **five-implementation family** (web, native, menuband, M4L, remote phone) sharing one social-software contract: the QWERTY-as-chromatic keymap. The web piece — historically the canonical implementation — has become the *laggard*: it has the worst audio latency on the platform (a measured **417 ms vs. <30 ms target**, 14× over budget), it lacks the sample-record / reverse-tape / FX-row work that landed in native over the past month, and the menuband app has overtaken it on UX polish (waveform strip, voice palette LCD, chord finder, localization). 14 + 15 + The three papers (`notepat`, `keymaps`, `plork`) all hinge on a single invariant — **the keymap** — and treat everything else (synth, recording, network, hardware) as substrate-specific. That gives us permission to stop chasing literal feature parity and instead **specify the invariant explicitly, then position each implementation to its substrate's strengths**: web = gateway / share / pedagogy; native = performance instrument; menuband = always-on desktop companion; M4L = DAW citizen; remote = controller. 16 + 17 + The critical near-term work is **(1)** finishing the AudioWorklet+WASM browser prototype to close the latency gap (commit `23c848771` started this — it's the only path back to a playable web), and **(2)** writing a `notepat-spec.md` so capabilities propagate by spec rather than by copy/paste. 18 + 19 + --- 20 + 21 + ## 1. The five implementations 22 + 23 + | # | Implementation | Path | Substrate | LOC | Posture | 24 + |---|---|---|---|---|---| 25 + | 1 | **Web** (notepat.com) | `system/public/aesthetic.computer/disks/notepat.mjs` | Browser, Web Audio | ~8,737 | Reference / gateway | 26 + | 2 | **Native** (AC Native OS) | `fedac/native/pieces/notepat.mjs` | QuickJS on bare metal, ALSA | ~7,259 | Performance instrument, default boot piece | 27 + | 3 | **Menuband** | `slab/menuband/Sources/MenuBand/` (Swift) | macOS menubar, AVFoundation | — | Always-on desktop companion | 28 + | 4 | **M4L** | `system/public/m4l/notepat.com.amxd` + recent worklet build | Ableton Live | — | DAW citizen | 29 + | 5 | **Remote** | `system/public/aesthetic.computer/disks/notepat-remote.mjs` | Phone web piece, UDP relay → Max bridge | — | Controller / second screen | 30 + 31 + Plus: `notepat-tv.mjs` (HDMI visualizer), `ac-electron` overlay mode (transparent floating widget — `402d953d9`), and offline WASM shells under `system/public/ac-native-wasm/notepat.html`. 32 + 33 + The slide at `slides/notepat-keymap/template.html` already enumerates this registry. It is the closest thing we have to a published spec. 34 + 35 + --- 36 + 37 + ## 2. Capability matrix 38 + 39 + Rows = capabilities. Cells = ✅ shipped, 🟡 partial, ❌ absent, — N/A for substrate. 40 + 41 + | Capability | Web | Native | Menuband | M4L | Remote | 42 + |---|:-:|:-:|:-:|:-:|:-:| 43 + | **The keymap** (QWERTY → chromatic, two-octave hand layout) | ✅ | ✅ | ✅ | ✅ | ✅ | 44 + | Self-documenting (letters name notes) | ✅ | ✅ | ✅ | ✅ | ✅ | 45 + | Multi-touch / pointer pads | ✅ | ✅ | 🟡 (mouse only) | — | ✅ | 46 + | Octave shift (1–9 + . , /) | ✅ | ✅ | ✅ (shift/capslock linger) | ✅ | ✅ (1-9 hot-switch) | 47 + | **Synth voices** | 8 (sine/tri/saw/sq/harp/whistle/composite/stample) | 8+ Salamander Grand Piano sample bank | GM 128+ via MIDISynth | host-defined | — (MIDI only) | 48 + | Drum kit / percussion mode | ✅ (9th mode) | ✅ (drum mode swaps upper octave) | 🟡 (loaded, not exposed) | — | ❌ | 49 + | **Sample recording into per-key banks** | ❌ | ✅ (End-key arms) | ❌ | ❌ | ❌ | 50 + | **Reverse tape / spacebar scrollback** | ❌ | ✅ (frozen ring between gestures) | ❌ | ❌ | ❌ | 51 + | Mic hot-input | ❌ | ✅ (device autodetect) | ❌ | host-managed | ❌ | 52 + | FX rows (room/glitch/drive/wobble/flange) | 🟡 (room/glitch only) | ✅ (full row + per-FX `0` reset) | ❌ | host-managed | ❌ | 53 + | Sustain (Shift-decay 6 s, Enter-latch) | ✅ | ✅ | 🟡 | ✅ | 🟡 | 54 + | Metronome (UTC-synced) | ✅ | ✅ (no spacebar gap) | ❌ | host-driven | ❌ | 55 + | Autopat / song mode | ✅ | ✅ | ❌ | ❌ | ❌ | 56 + | Hover overlay | ✅ | ✅ | ✅ (HoverLink chips, voice pill) | — | ❌ | 57 + | MIDI in | ✅ (relay socket) | ✅ (USB) | ✅ (Virtual "Menu Band" source) | ✅ | ✅ (M4L bridge) | 58 + | MIDI out | ❌ | ✅ (UDP heartbeat → relay) | ✅ (publishes virtual source) | ✅ | ✅ | 59 + | **Audio latency** | **~417 ms** ⚠️ | **<10 ms** (ALSA mmap + CPU pin) | **near-zero** (AVAudioEngine) | host-bounded | network-bounded | 60 + | AudioWorklet/WASM path | 🟡 (prototype `23c848771`) | — | — | started | — | 61 + | Theming | octave colors | octave + handle colors | light/dark + Terminal | — | track-color flavored board | 62 + | Bandmate sprites (Piano Man / Sample Sally / harpist) | ✅ (eye-tracking) | ✅ (per-wave) | ❌ | ❌ | ❌ | 63 + | Localization | ❌ | ❌ | ✅ scaffold | ❌ | ❌ | 64 + | Chord finder | ❌ (lib imported, unused) | ❌ | ✅ integrated | ❌ | ❌ | 65 + | Waveform/LED visualizer | ✅ (oscope) | ✅ | ✅ (Metal shader strip + popover) | host-side | ✅ | 66 + | NuPhy Air60 HE pressure → velocity | ✅ (WebHID) | ✅ (analog detect) | ❌ | — | — | 67 + | Network multiplayer / ensemble | 🟡 (UDP to notepat-tv) | 🟡 (UDP MIDI broadcast) | ❌ | session-server | ✅ (M4L bridge) | 68 + | Recall / undo | ❌ | ❌ | ❌ | host | ❌ | 69 + 70 + The matrix surfaces the shape of the problem clearly: **web has the broadest visual feature set but the worst latency and no recording**; native has caught up on visuals and overtaken on audio engine + recording; menuband has eclipsed both on UX polish and is the only one with localization or a chord finder. 71 + 72 + --- 73 + 74 + ## 3. The papers' rubric 75 + 76 + The three papers commit to a single thesis: notepat works because it externalises one social-software object (the keymap) onto every available substrate, and lets the substrate do what it's good at. From them we can extract a non-negotiable invariant set: 77 + 78 + 1. **Keymap is identical across implementations.** QWERTY letters name notes; left hand = lower octave, right hand = upper; dedicated rows for sharps. (`keymaps.tex`) 79 + 2. **Self-documenting.** No manual required. Anyone who knows C-D-E-F-G-A-B knows the layout. (`notepat.tex` + `keymaps.tex`) 80 + 3. **Playable on first power-on.** No configuration; no install (or, for native, *boot is the install*). (`plork.tex`) 81 + 4. **Built-in synthesis at the substrate's native floor.** Web Audio for browser, ALSA mmap for bare metal, AVFoundation for macOS. The instrument *uses the substrate as instrument*. (`notepat.tex`) 82 + 5. **Diatonic letter inheritance.** Western pedagogy notation is preserved; notation cost = 0. (`keymaps.tex`) 83 + 6. **Forkable.** A keymap is a small declarative table. The platform must let users author alternatives (this is the Lialina "Turing Complete User" claim and is currently *unmet*). (`keymaps.tex`) 84 + 7. **Convivial.** Used by anyone, without specialised training, to accomplish purposes they determine. (`plork.tex` quoting Illich.) 85 + 86 + Things the papers explicitly leave to the implementation: 87 + 88 + - Synthesis voices (the menu can vary per substrate) 89 + - Recording / sequencing affordances 90 + - Network / multiplayer model 91 + - Visual identity / theme 92 + - Hardware probes (NuPhy, USB MIDI, mic, deck) 93 + - Pedagogy surface (song mode, autopat) 94 + 95 + In other words: **the papers permit divergence on every axis except the keymap**. We are allowed to let web, native, and menuband each be best at different things. 96 + 97 + --- 98 + 99 + ## 4. Diagnosis: why web is behind, and what kind of "behind" it is 100 + 101 + There are three different gaps stacked on top of each other and they need different treatments: 102 + 103 + **Gap A — Audio floor (existential).** Web latency is 417 ms ([`notepat-audio-latency-report.md`](notepat-audio-latency-report.md)); the paper's pedagogy claim and the `plork` ensemble claim both require <30 ms. Native solved this with ALSA mmap + audio CPU pin (`9cf58384a`). Menuband never had the problem (AVAudioEngine). Web has a half-finished AudioWorklet+WASM prototype (`23c848771`). **Until this gap closes, every other feature added to web is being added to an instrument that fails the papers' minimum playability test.** 104 + 105 + **Gap B — Audio engine richness (real but bounded).** Native has shipped the Salamander Grand Piano sample bank (`e4895fc02`), a proper Karplus-Strong banded waveguide piano (`b0815cc06`), wobble/flange (`66452b605`), drive with tanh soft-sat (`ea57b7b77`), zoo + laser kits with researched DSP (`fa1a60f65`), per-FX `0` reset (`7c67fc6c7`), reverse-only spacebar loop with frozen-history-between-gestures (`b407b7a2b`, `510ae5a1a`). Web has gotten harp + whistle ported back (`596dfbc1b`) and shares percussion via `lib/percussion.mjs` (`1828b31db`) — that's the right precedent — but the rest is one-way. **The fix is ongoing back-porting via shared libs.** 106 + 107 + **Gap C — UX vocabulary (small but visible).** Menuband has chord finder, voice palette LCD, waveform strip, hover-link chips, localization scaffold. Web has none of these and the menuband team is moving fast. **This isn't urgent — menuband is allowed to lead on desktop UX — but a few of these (chord finder, waveform strip in the spirit of menuband's) make sense to mirror in the web piece since the lib is already imported and unused.** 108 + 109 + There is also a **structural gap**: there is no shared spec. Capabilities have propagated by copy-paste between five implementations. The slide at `slides/notepat-keymap/template.html` is the closest we have to a published contract, and it's a slide. This is fine for three implementations; with five (and a sixth coming if VST3 happens), **it stops scaling**. 110 + 111 + --- 112 + 113 + ## 5. Strategic options 114 + 115 + **Option A — Backport-first.** Treat web as an implementation that needs to catch up. Port sample recording, reverse tape, FX rows, Salamander, chord finder. Pro: keeps the "five implementations of one piece" story tidy. Con: web's latency makes most of these features feel worse than on native; you'd be polishing a slow instrument. 116 + 117 + **Option B — Audio engine consolidation via shared lib.** Extend the `lib/percussion.mjs` precedent: factor each voice (harp, whistle, piano, drums, FX chain) into a substrate-agnostic spec under `shared/notepat/voices/`. Each substrate adapts: web → AudioWorklet, native → ALSA, menuband → AVFoundation. Pro: structurally sound; addresses Gap B properly. Con: large refactor; doesn't fix Gap A. 118 + 119 + **Option C — Latency floor first.** Finish the AudioWorklet+WASM prototype before any new features land in web. Pro: required by the papers; unblocks everything else. Con: hard, deeply technical work in browser audio. 120 + 121 + **Option D — Repositioning.** Stop trying to bring web to feature parity. Curate it to its strengths (URL-shareable, no-install, instant-share, song mode for kids, gateway-into-the-platform) and let native + menuband own the performance-instrument and desktop-companion roles. Publish this as a *design decision*, not a regression. Pro: honest about what each substrate is for; aligns with the papers' permission to diverge. Con: requires a public framing change for notepat.com. 122 + 123 + **Option E — Spec-first.** Write `notepat-spec.md` (under `shared/` or `papers/`) and treat each implementation as a *conformance target*. Mark each capability as REQUIRED (the keymap, self-documenting, synthesis at substrate floor) vs. RECOMMENDED (FX rows, hover) vs. OPTIONAL (mic, deck, kiosk). Pro: turns ad-hoc feature drift into intentional divergence; catches up the keymaps.tex "forkable" claim. Con: bureaucracy if over-applied. 124 + 125 + --- 126 + 127 + ## 6. Recommendation 128 + 129 + **Do C + E + B in that order, plus D as a public framing.** Skip A. 130 + 131 + 1. **Finish AudioWorklet+WASM web build (Gap A).** This is the existential blocker. Until web hits <30 ms, nothing else matters. The prototype exists (`23c848771`); the path is known. 132 + 2. **Write `papers/notepat-spec.md` (or `shared/notepat/spec.md`)** that names the keymap as the only true REQUIRED, lists RECOMMENDED capabilities (drum mode, sustain semantics, octave-shift keys), and OPTIONAL ones (sample recording, mic, deck, NuPhy pressure, kiosk). Make it short — one page. Put the slide diagram in it. 133 + 3. **Land Option D as a paragraph on notepat.com** — "web notepat is the gateway; the desktop companion is menuband; the studio instrument is native. They share a keymap." Stop framing native and menuband as "ahead"; frame them as *different organs of the same instrument*. 134 + 4. **Then begin Option B (shared voices lib).** Salamander Grand Piano, harp, whistle, drum kit, FX chain — substrate-agnostic spec, three adapters. Use `lib/percussion.mjs` as the template. 135 + 5. **Defer Option A** (broad backport into web) indefinitely. Backport selectively only when the shared-voices refactor naturally lifts a feature into web for free. 136 + 137 + Two specific things to ship cheaply along the way because the cost is near-zero: 138 + - Wire up the `lib/chord-detection.mjs` import that's already sitting in `notepat.mjs` (currently imported and unused). Menuband already has chord finder — getting web to display chord names while you're playing is a 50-line diff. 139 + - Mirror menuband's waveform strip on web. The oscilloscope (scope=16) already exists; the menuband design just makes it readable. 140 + 141 + --- 142 + 143 + ## 7. Concrete next-step queue (ranked) 144 + 145 + 1. **AudioWorklet+WASM web build to playable state** (closes Gap A). Owner: web. Blocker on everything else. 146 + 2. **`notepat-spec.md`** (one page, REQUIRED/RECOMMENDED/OPTIONAL). Owner: jeffrey. Two hours of writing. 147 + 3. **notepat.com landing copy update** explicitly positioning web vs. native vs. menuband vs. M4L vs. remote. Half a day. 148 + 4. **Shared voices lib scaffold** — `shared/notepat/voices/{harp,whistle,piano,drum,fx}.mjs` with adapters. Move existing `lib/percussion.mjs` into it. 149 + 5. **Wire `chord-detection.mjs` into web** (already imported, never called). 150 + 6. **Menuband-style waveform strip in web** (visual parity, cheap). 151 + 7. **`notepat-keymap` slide → `notepat-spec.md` figure**, so the registry is part of the spec, not just a slide. 152 + 8. **Forkable keymaps as a real feature.** keymaps.tex commits to this; no implementation supports it yet. Could be as simple as a URL param `?keymap=dvorak` that loads a JSON table from `shared/notepat/keymaps/`. 153 + 9. **Recording-as-feature decision.** Sample recording exists only in native; the papers don't require it. Either backport (via shared lib) or formalise that the studio instrument records and the web instrument shares URLs. Pick one. 154 + 10. **`notepat-tv` is a separate piece — keep it that way**, but document it in the spec as "OPTIONAL secondary display target." 155 + 156 + --- 157 + 158 + ## 8. Things to flag 159 + 160 + - **The latency report is from a single measurement run.** Re-measure on the AudioWorklet prototype before deciding the fix worked. The stability report explicitly notes that latency telemetry isn't currently collected after code reloads. 161 + - **The slide has five implementations; the papers describe three substrate classes (browser, bare metal, host).** M4L and remote are sub-cases. Worth deciding in the spec whether they're "implementations" or "channels." 162 + - **`notepat-remote.mjs` has had ~30 commits in the last few months** — it's the most actively iterated implementation after menuband. Worth a separate mini-report on what it's converging toward. 163 + - **`drum mode swaps only upper octave` (`8f629669b`) is a native-only behaviour.** Web's drum mode (the 9th wave) replaces the entire keyboard. Pick one and put it in the spec. 164 + - **The spec doc doesn't exist yet.** Until it does, every conversation about "is X a notepat feature" is going to be re-litigated per implementation. 165 + 166 + --- 167 + 168 + ## 9. References 169 + 170 + - `system/public/aesthetic.computer/disks/notepat.mjs` (web, ~8,737 LOC) 171 + - `fedac/native/pieces/notepat.mjs` (native, ~7,259 LOC) 172 + - `slab/menuband/Sources/MenuBand/` (Swift macOS app, v0.9) 173 + - `system/public/menuband/index.html` (landing only) 174 + - `slides/notepat-keymap/template.html` (registry slide) 175 + - `papers/arxiv-notepat/notepat.tex` (origin / instrument-platform co-evolution) 176 + - `papers/arxiv-keymaps/keymaps.tex` (keymaps as social software) 177 + - `papers/arxiv-plork/plork.tex` (planetary laptop orchestra; notepat as default boot piece) 178 + - `reports/notepat-audio-latency-report.md` (417 ms measurement) 179 + - `reports/notepat-stability.md` (3,229-note stress test passed) 180 + - `reports/2026-04-10-notepat-percussion-and-dj-hotplug.md` (drum kit + USB hotplug) 181 + - `reports/2026-04-11-notepat-ota-usb-flash-report.md` (OTA flash regression) 182 + - Key commits: `23c848771` (AudioWorklet WASM prototype), `1828b31db` (shared percussion lib precedent), `596dfbc1b` (harp + whistle backport), `e4895fc02` (Salamander), `9cf58384a` (ALSA mmap + CPU pin), `b407b7a2b` (frozen reverse-replay history), `82440e7c8` (menuband i18n + hover chips), `13c6bdfb2` (menuband chord finder).
+45 -9
slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift
··· 8 8 /// to Terminal, so the per-tick refresh only fires osascript when 9 9 /// something actually changed. 10 10 private var lastTerminalDecor: [String: String] = [:] 11 + /// Base font size from the most recent `tileNow()` pass. `applyTerminalDecor` 12 + /// scales typography off this — `.awaiting` ("orange") tiles get bumped 13 + /// up so focus reads typographically while the cell geometry stays put. 14 + private var lastTiledFontSize: Int? 11 15 private var rainbowPhase: CGFloat = 0 12 16 private var rotationPhase: CGFloat = 0 13 17 private var mailTickCount = 0 ··· 351 355 // applying a profile resets the tab's font + the window's pixel 352 356 // size to the profile default, causing a brief reframe-flicker 353 357 // even when we save/restore around it. 354 - struct Assignment { let tty: String; let bg: (Int, Int, Int)?; let title: String } 358 + struct Assignment { let tty: String; let bg: (Int, Int, Int)?; let title: String; let fontSize: Int? } 355 359 var changes: [Assignment] = [] 356 360 var seen = Set<String>() 361 + // Typographic gradient by state — green smallest (calm), red largest 362 + // (escalate). The base size comes from the most recent tile pass, so 363 + // the global Near/Far toggle naturally shifts every tier in lockstep. 364 + // Tweak these freely — the goal is that a sweep across the wall reads 365 + // its priority just from the size. 366 + // working (green) 1.00× — head-down, default 367 + // complete (slate) 1.10× — turn done, gentle "look when ready" 368 + // awaiting (orange) 1.25× — paused for input, focus here 369 + // stale (red) 1.45× — process dead, biggest call for cleanup 370 + let baseFont = self.lastTiledFontSize 371 + func sizeFor(_ scale: Double) -> Int? { 372 + baseFont.map { Int((Double($0) * scale).rounded()) } 373 + } 357 374 for s in state.claudeSessions where !s.tty.isEmpty { 358 375 seen.insert(s.sessionId) 359 376 let bg: (Int, Int, Int)? 360 377 let glyph: String 378 + let fontSize: Int? 361 379 switch s.state { 362 380 // Working = green (active/healthy), complete = calm slate 363 381 // (turn done, idle), awaiting = amber (needs you to continue), 364 - // stale = no bg change. RGBs are 0–65535 AppleScript colorspace, 365 - // dark enough that the profile's default light text still reads. 366 - case .working: bg = (1500, 14000, 4000); glyph = "● working" // deep forest green 367 - case .complete: bg = (5000, 7000, 12000); glyph = "✓ complete" // muted slate 368 - case .awaiting: bg = (32000, 18000, 1500); glyph = "◉ awaiting" // warm amber — attention! 369 - case .stale: bg = nil; glyph = "○ stale" // leave bg alone 382 + // stale = deep red (process dead, escalate). RGBs are 0–65535 383 + // AppleScript colorspace, dark enough that the profile's default 384 + // light text still reads on top. 385 + case .working: bg = (1500, 14000, 4000); glyph = "● working"; fontSize = sizeFor(1.00) // green — smallest 386 + case .complete: bg = (5000, 7000, 12000); glyph = "✓ complete"; fontSize = sizeFor(1.10) // slate — between green and orange 387 + case .awaiting: bg = (32000, 18000, 1500); glyph = "◉ awaiting"; fontSize = sizeFor(1.25) // orange — focus pop 388 + case .stale: bg = (30000, 2500, 4000); glyph = "○ stale"; fontSize = sizeFor(1.45) // red — largest, escalate 370 389 } 371 390 let title = "\(glyph) · \(s.titleString)" 372 391 let bgKey = bg.map { "\($0.0),\($0.1),\($0.2)" } ?? "-" 373 - let key = "\(bgKey)|\(title)" 392 + let key = "\(bgKey)|\(title)|\(fontSize.map(String.init) ?? "-")" 374 393 if lastTerminalDecor[s.sessionId] == key { continue } 375 394 lastTerminalDecor[s.sessionId] = key 376 - changes.append(Assignment(tty: s.tty, bg: bg, title: title)) 395 + changes.append(Assignment(tty: s.tty, bg: bg, title: title, fontSize: fontSize)) 377 396 } 378 397 // Reap entries for sessions that disappeared since last tick — they 379 398 // either died or got reaped by the janitor; either way our memo is ··· 404 423 lines.append(" set background color of t to {\(bg.0), \(bg.1), \(bg.2)}") 405 424 } 406 425 lines.append(" set custom title of t to \"\(escTitle)\"") 426 + // Font sets snap Terminal to the row/col grid, so save the window's 427 + // bounds and restore them — typography changes per state, geometry 428 + // stays put with the tile. 429 + if let fs = a.fontSize { 430 + lines.append(" set savedBounds to bounds of w") 431 + lines.append(" set font size of t to \(fs)") 432 + lines.append(" set bounds of w to savedBounds") 433 + } 407 434 lines.append(" end if") 408 435 } 409 436 lines.append(contentsOf: [ ··· 560 587 ).output.trimmingCharacters(in: .whitespacesAndNewlines) 561 588 guard let n = Int(countOut), n > 0 else { return } 562 589 guard let layout = Self.computeTileLayout(count: n, geom: geom, near: near) else { return } 590 + 591 + // Remember the tile's base font + reset decor memo. The script 592 + // below resets every window to base; without this, a session 593 + // already in `.awaiting` would still match its old "we pushed 594 + // the bumped font" key and skip the re-bump on the next refresh. 595 + DispatchQueue.main.async { [weak self] in 596 + self?.lastTiledFontSize = layout.fontSize 597 + self?.lastTerminalDecor.removeAll() 598 + } 563 599 564 600 var lines: [String] = ["tell application \"Terminal\"", " activate"] 565 601 for i in 0..<n {