app
Hero Studio
Public Made by Adomby adom
The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Every hero wears its P
← Commit history
Publish 0.11.1
7 files changed
+120−29
data/pages.json+26@@ -1,4 +1,30 @@ [+ {+ "type": "app",+ "title": "COSMIIC Implantables",+ "author": "",+ "score": 0,+ "stars": 0,+ "installs30d": 0,+ "brief": "The COSMIIC implantables project as a ready-to-open Adom 3D editor layout",+ "tags": [],+ "updated_at": "2026-07-30T20:05:23.149Z",+ "owner": "cosmiic",+ "slug": "implantables"+ },+ {+ "type": "app",+ "title": "COSMIIC WireBender",+ "author": "",+ "score": 0,+ "stars": 0,+ "installs30d": 0,+ "brief": "The COSMIIC firmware development bench as a ready-to-open Adom 3D editor layout",+ "tags": [],+ "updated_at": "2026-07-30T19:59:32.597Z",+ "owner": "cosmiic",+ "slug": "wirebender"+ }, { "type": "app", "title": "US History Jeopardy",
doctor.mjs+1−1@@ -203,6 +203,6 @@ export function printReport(report, out = console.error) { if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { const report = await runChecks(); printReport(report);- console.log(JSON.stringify({ ok: report.ok, checks: report.checks }));+ console.log(JSON.stringify({ ok: report.ok, renderer: report.renderer, checks: report.checks })); process.exit(report.ok ? 0 : 1); }
install.sh+22−13@@ -8,16 +8,25 @@ echo "run 'hero-studio' (serves on port 8846), then open /proxy/8846/ in a webvi echo "refresh the sample data with ./refresh-data.sh (needs the adom-wiki CLI)" # Renderer preflight, warn mode: install stays exit 0 (driving the studio and-# fixing deps later are legitimate on light containers), but a broken renderer-# must be impossible to read as success. Generate spawns headless Chrome INSIDE-# this container; light images (Hydrogen Desktop) do not ship that runtime.-if node "$PWD/doctor.mjs" >/dev/null 2>&1; then- echo "renderer: OK (headless Chrome + stamping verified; Generate will work)"-else- echo ""- echo "!! RENDERER_NOT_READY -- installed, BUT GENERATE WILL FAIL IN THIS CONTAINER !!"- echo "!! Do NOT open the pose gate for a human yet: run 'hero-studio doctor', !!"- echo "!! run the fix commands it prints (you have the shell; no SSH needed), !!"- echo "!! and re-run it until it passes. pose refuses to open until then. !!"- node "$PWD/doctor.mjs" 2>&1 | sed 's/^/ /' || true-fi+# fixing deps later are legitimate on light containers), but the message must+# say which renderer actually verified. "OK" while local Chrome was missing is+# how hero-studio#8 happened: the pup fallback carried a fresh install further+# than its fidelity deserved, silently.+DOCTOR_JSON="$(node "$PWD/doctor.mjs" 2>/dev/null || true)"+case "$DOCTOR_JSON" in+ *'"renderer":"local"'*)+ echo "renderer: OK (local headless Chrome verified; Generate renders in-container)" ;;+ *'"renderer":"pup"'*)+ echo ""+ echo "!! renderer: PUP FALLBACK ONLY. The local headless-Chrome stack is missing, !!"+ echo "!! so Generate will render through the HOST browser: it works, at reduced !!"+ echo "!! fidelity, and only while Adom Desktop is running. For the full local !!"+ echo "!! renderer run 'hero-studio doctor' and apply the fixes it prints. !!" ;;+ *)+ echo ""+ echo "!! RENDERER_NOT_READY -- installed, BUT GENERATE WILL FAIL IN THIS CONTAINER !!"+ echo "!! Do NOT open the pose gate for a human yet: run 'hero-studio doctor', !!"+ echo "!! run the fix commands it prints (you have the shell; no SSH needed), !!"+ echo "!! and re-run it until it passes. pose refuses to open until then. !!"+ node "$PWD/doctor.mjs" 2>&1 | sed 's/^/ /' || true ;;+esac
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.11.0",+ "version": "0.11.1", "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
page.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.11.0",+ "version": "0.11.1", "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
render-pup.mjs+45−13@@ -56,8 +56,15 @@ await sleep(2000); // 5. measure #hero in viewport pixels + devicePixelRatio. Numbers only, // joined with commas: no quotes means no escaping ambiguity in the transport.+// Also paint a sentinel square in the viewport's top-left corner: the crop+// step verifies it is present in the screenshot, proving the relay shot THIS+// window and not whatever was focused (hero-studio#8: a relay that ignores+// sessionId captures the human's interactive studio, tooltips and all). const boxRaw = await evalIn(- "(function(){var b=document.querySelector('#hero').getBoundingClientRect();" ++ "(function(){var s=document.getElementById('hs-sentinel');if(!s){s=document.createElement('div');" ++ "s.id='hs-sentinel';s.style.cssText='position:fixed;top:0;left:0;width:16px;height:16px;" ++ "background:#ff00fe;z-index:2147483647';document.body.appendChild(s);}" ++ "var b=document.querySelector('#hero').getBoundingClientRect();" + "return 'HSBOX@'+[b.x,b.y,b.width,b.height,window.devicePixelRatio].join(',')+'@XOBSH';})()"); const m = boxRaw && boxRaw.match(/HSBOX@([\d.,\-]+)@XOBSH/); if (!m) die('could not measure #hero (unexpected browser_eval output).');@@ -80,32 +87,57 @@ if (!srcPng) { } if (!srcPng) die('browser_screenshot returned neither a readable path nor base64 (raw head: ' + shot.out.slice(0, 200) + ')'); -// 7. crop #hero out (viewport rect * dpr, clamped) and sanity-check aspect.-// The screenshot is assumed to be the page viewport; if the host returns a-// native window shot including chrome, the crop will land off and the aspect-// warning below is the tell.+// 7. verify the sentinel, then crop #hero out (viewport rect * dpr, clamped).+// Both checks are HARD failures: a hero that would fail the registry gate+// must never be handed to the human as a success (hero-studio#8). const py = [ 'import sys', 'from PIL import Image', 'src, out = sys.argv[1], sys.argv[2]', 'x, y, w, h, dpr = [float(v) for v in sys.argv[3:8]]',- 'im = Image.open(src)',- 'box = (max(0, round(x*dpr)), max(0, round(y*dpr)), min(im.size[0], round((x+w)*dpr)), min(im.size[1], round((y+h)*dpr)))',+ 'im = Image.open(src).convert("RGB")',+ 'W, H = im.size',+ '# locate the 16px #ff00fe sentinel painted at viewport (0,0). Its found',+ '# position IS the viewport origin inside the screenshot: a native window',+ '# shot includes chrome above the viewport, which would silently shift the',+ '# crop while keeping its aspect, so the origin must come from the pixels.',+ 'ox = oy = -1',+ 'strip = im.crop((0, 0, min(W, 240), min(H, 480)))',+ 'px = strip.load()',+ 'for yy in range(strip.size[1]):',+ ' for xx in range(strip.size[0]):',+ ' r, g, b = px[xx, yy]',+ ' if r > 200 and g < 80 and b > 200:',+ ' ox, oy = xx, yy',+ ' break',+ ' if ox >= 0:',+ ' break',+ 'if ox < 0:',+ ' print("NO-SENTINEL"); sys.exit(3)',+ 'box = (max(0, ox + round(x*dpr)), max(0, oy + round(y*dpr)), min(W, ox + round((x+w)*dpr)), min(H, oy + round((y+h)*dpr)))', 'im2 = im.crop(box)',+ 'aspect = im2.size[0] / im2.size[1]',+ 'if abs(aspect - 1.6) > 0.03:',+ ' print("BAD-ASPECT %dx%d" % im2.size); sys.exit(4)', 'im2.save(out)', 'print("%dx%d" % im2.size)', ].join('\n'); const crop = await new Promise((res) => { execFile('python3', ['-c', py, srcPng, out, String(bx), String(by), String(bw), String(bh), String(dpr)],- { timeout: 30000 }, (err, stdout, stderr) => res({ ok: !err, out: String(stdout || ''), err: String(stderr || '') }));+ { timeout: 30000 }, (err, stdout, stderr) => res({ code: err ? (err.code || 1) : 0, out: String(stdout || ''), err: String(stderr || '') })); }); if (srcPng === out + '.full.png') { try { fs.unlinkSync(srcPng); } catch {} }-if (!crop.ok || !fs.existsSync(out)) die('PIL crop failed: ' + crop.err.trim().split('\n').pop());-const [cw, ch] = crop.out.trim().split('x').map(Number);-const aspect = cw / ch;-if (Math.abs(aspect - 1.6) > 0.03) {- console.error('warning: cropped ' + cw + 'x' + ch + ' (aspect ' + aspect.toFixed(3) + ', want 1.600 +-0.03): the hero may have been clipped by the host viewport; enlarge the host window and regenerate.');+if (crop.code === 3) {+ die('the screenshot does NOT contain the render window (sentinel missing: ' + crop.out.trim() + '). ' ++ 'The relay captured a different window than the hero-render session, so the shot was discarded; ' ++ 'this is the wrong-window class from hero-studio#8. Report which adom-desktop build this is.');+}+if (crop.code === 4) {+ die('cropped ' + crop.out.trim().replace('BAD-ASPECT ', '') + ', not 16:10 within 0.03: the hero was clipped by the host viewport. ' ++ 'Enlarge the host render window and regenerate; refusing to ship a hero the registry would reject.'); }+if (crop.code !== 0 || !fs.existsSync(out)) die('PIL crop failed: ' + crop.err.trim().split('\n').pop());+const [cw, ch] = crop.out.trim().split('x').map(Number); if (cw < 1600) { console.error('warning: cropped width ' + cw + ' is under the gate\'s 1600px floor; the finalize upscale will cost sharpness. A larger host window or higher devicePixelRatio fixes it.'); }
server.js+24@@ -288,6 +288,22 @@ const server = http.createServer((req, res) => { return; function finalize(renderedVia) {+ // refuse to hand the human a hero the registry will reject: the gate+ // wants 16:10 within 0.03, and a bad capture (wrong window, clipped+ // viewport) shows up here as a wrong aspect. PNG IHDR: width/height+ // big-endian at bytes 16/20.+ try {+ const head = fs.readFileSync(outFile);+ const w = head.readUInt32BE(16);+ const h = head.readUInt32BE(20);+ if (!(w > 0 && h > 0) || Math.abs(w / h - 1.6) > 0.03) {+ logLine('error', 'generate via ' + renderedVia + ' produced ' + w + 'x' + h + ' (not 16:10); refusing to ship it');+ try { fs.unlinkSync(outFile); } catch {}+ return finish({ ok: false, error: renderedVia + ' render produced ' + w + 'x' + h + ', not 16:10; the registry would reject it, so it was discarded', hint: renderedVia === 'pup' ? 'the host capture was clipped or hit the wrong window; enlarge the host render window and retry, and report what render-pup printed on stderr' : "run 'hero-studio doctor' and retry" });+ }+ } catch (e) {+ return finish({ ok: false, error: 'could not read the rendered PNG: ' + e.message, hint: "run 'hero-studio doctor'" });+ } // downscale to the hero standard 2000x1250 and stamp provenance metadata const meta = JSON.stringify({ generator: 'hero-studio', version: PKG_VERSION, ts, renderer: renderedVia, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, fontSize: state.fontSize, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined } }); const py = 'import sys\n' +@@ -345,4 +361,12 @@ const server = http.createServer((req, res) => { }); }); +server.on('error', (err) => {+ if (err && err.code === 'EADDRINUSE') {+ console.error('hero-studio: a server is already running on :' + PORT + ' (that one keeps working).');+ console.error("talk to it with 'hero-studio state', or stop it first to start a fresh one.");+ process.exit(1);+ }+ throw err;+}); server.listen(PORT, () => console.log('hero-studio on ' + PORT));