95 Commits
Author SHA1 Message Date
Hanzo AI b89cf707a2 studio v0.18.3: monochrome shell chrome — coherent with the Home + @hanzo/gui design system
shell.css (chat sidebar · Developers dock · GPUs panel) was still on the old
charcoal palette; snap it to the canonical monochrome tokens the Home already
uses: page #000 · surface #0a0a0a · raised #1a1a1a · border #1f1f1f · text
#ededed · dim #888 · brand/CTA #fff. Semantic green (online) / red (error) kept.
The shared shell now matches the create experience + the chat's own empty-state
and friendly-error work already on main.
2026-07-25 10:17:15 -07:00
Hanzo AI 99720f2bf4 studio: monochrome create experience — design-system mode, wizard, agentic builder, template picker
The Studio Home 'What should we create?' surface, fully monochrome and DRY on
the canonical design tokens + icon()/_popover:

- Line-icon system (icon(name) → currentColor SVG). No emoji anywhere in the
  create experience; template cards + tabs + chat button all use lucide-style
  monochrome marks.
- Searchable template picker (Template chip → .tpick): every blueprint grouped,
  filterable, keyboard-navigable; None returns to edit/compose.
- Design systems mode (new tab): reusable brand styles a generation can pull
  from — preset systems (Modernist/Classical/Nocturne/Organic/Broadsheet/
  Industry) with Use / Set org default, plus Create design system.
- Set up your design system wizard (#dswiz): company blurb + link a public
  GitHub repo (→ e.g. github.com/hanzoai/design), a local folder, a .fig
  (parsed in-browser, never uploaded), or fonts/logos.
- Agentic design-system builder (#bld): chat + todos + file tree + editor to
  generate a system from those inputs.

shell.js: Chat dock button uses a monochrome SVG icon (was emoji).
2026-07-24 23:06:38 -07:00
Hanzo AI 49b8d1ad0f studio: monochrome shell theme + Copilot chat, Enso default
De-purple the ComfyUI shell theme (hanzo-theme.css) and the Copilot sidebar
chat (hanzo-copilot.css) to the canonical monochrome design system — no color
accent. Accent tokens map to neutral (icons/links/active #ededed, primary
buttons white-on-dark with #111 text like the Home CTA, selected/checked
neutral gray, focus ring #8a8a8a). Semantic status colors are unaffected.

Copilot default model 'zen' -> 'enso' (the Hanzo flagship assistant) —
Enso default everywhere, including Studio.
2026-07-24 17:14:30 -07:00
Hanzo AI c20b6b6fa5 studio: real org/user/project switcher + @hanzo/gui design tokens on Home
Collapse the Studio Home account pill's three overlapping mechanisms (static
#acctmenu, dynamic #omenu, two competing #user handlers) into ONE identity
popover rendered through the same openMenu the composer pills use. It shows who
you are (avatar/name/email) and switches organization AND project against the
same /v1/session API the node editor uses (re-scopes tenancy + output paths on
reload). Pill now reads avatar · name · org · caret.

openMenu gains a headNode + per-item {on,href,act} so one popover serves the
composer pills AND the identity menu (backward-compatible — existing pill call
sites unchanged).

Snap the palette to the canonical neutral Hanzo design system (hanzo.app /
hanzo.ai): page #000 · surface/popover #0a0a0a · raised/hover #1a1a1a · border
#1f1f1f · text #ededed · dim #888 · brand/CTA #fff · ring #333. Every dropdown
panel (.pmenu, .ovfmenu) uses the one canonical menu spec: 10px radius + the
two-layer shadow. Fix the latent feedback-bubble contrast (white-on-white).
2026-07-24 17:00:07 -07:00
Hanzo AI 09dc7af5ab test(studio): split semicolon statements — green the ruff gate on main
The Python Linting gate (ruff check .) had been RED on main across multiple
commits: 5x E702 (multiple-statements-on-one-line) in studio_home_fix_test.py.
Split each `x = ...; x.write_bytes(...)` onto two lines. Behavior identical
(19/19 pass); no other files affected.
2026-07-24 16:58:10 -07:00
Hanzo AI f9ea2d2eda merge: studio composer dropdowns + unified Chat → main
Ships fix/studio-chat-and-dropdowns: real Design/Template/</>/Model dropdowns on
the /studio composer and Chat unified on the @hanzo/ai contract, with the three
review mustFix resolved (fail-closed pk- allowlist, chat stream→copilot fallback,
committed guard + SSE parser tests). No file overlap with the marketing-shell /
native-deploy work already on main.
2026-07-24 16:50:07 -07:00
Hanzo AI d75983c437 studio: fail-closed pk allowlist + chat stream fallback + parser/guard tests
Resolves the three review mustFix before a publishable key is provisioned to the
studio pod (the next deploy step), which flips on the streaming path and relies
on the key guard:

- M1 (security): _publishable is now an ALLOWLIST — emit only `pk-` (the
  gateway's single client-safe prefix, iamauth.go:472-476). The old denylist
  missed fw_/hz_/JWT server secrets and failed OPEN, leaking them into
  window.HZ.pk in page source.
- M2 (robustness): the /studio Chat stream degrades to the same-origin copilot on
  any transport failure (fetch throws / non-2xx / no body) instead of
  dead-ending, so a PK provisioned before the gateway allows CORS+Authorization
  for studio.hanzo.ai can't hard-break Chat. SSE parse decomplected into pure
  sseLines/sseDelta; stream behavior otherwise identical.
- M3 (coverage): commit publishable_test.py (allowlist + <script> break-out incl.
  U+2028/9) and sse_parser_test.py (runs the SHIPPED shell.js parser under node).

103/103 studio_home_test, 207/207 middleware_test green.
2026-07-24 16:49:17 -07:00
Hanzo AI 131081184a studio: real composer dropdowns + unify Chat on the @hanzo/ai contract
The "What should we create?" composer's four pills were broken: Design
system and Template were stub toasts / a tab-jump, </> was dead, and Model
was a hardcoded "Opus 4.8" with no handler. Replace all four with ONE
monochrome popover (open · real options · select · Esc/outside-close ·
keyboard), wired to real sources:

- Design system → the org's real designs (the SAME FILTER the gallery chips
  drive; pill and chips stay in sync via paintChips — one state, two controls)
- Template → the KIND catalog, grouped, reusing pickTemplate/clearLane
- </> → a read-only request preview from composerPlan(), the ONE planner
  create() now also executes (no drift; billable dispatch behavior unchanged)
- Model → the real catalog from api.hanzo.ai/v1/models, curated to the
  Studio set (enso · zen · image/video), shown only where the id truly
  exists upstream; default enso, persisted (hz_model), drives the Chat model

Chat sidebar (shell.js): stream enso from api.hanzo.ai/v1/chat/completions
(OpenAI-compatible SSE) when a PUBLISHABLE key is present, else the secure
same-origin copilot fallback (httpOnly cookie; keeps graph ops in the
editor). "Powered by Hanzo AI" footer; model follows the composer's pick.

Config injected as window.HZ from env (NEXT_PUBLIC_HANZO_PUBLISHABLE_KEY):
_publishable() fails closed — refuses to hand the browser anything that
looks like a server secret (sk-/hk-/secret*), so a misconfig drops to the
copilot path rather than leaking. No secret ever reaches page source.
2026-07-24 16:17:59 -07:00
Hanzo AI 9df0c317b7 studio: framed unauth navigation gets a same-origin connect card, not the IdP 302 — makes the console's Studio embed work (the IdP refuses framing) 2026-07-21 21:51:04 -07:00
Hanzo AI 8383230a98 studio: Developers workbench dock in the shared shell — Queue · Logs · read-only API Shell (both views) 2026-07-21 15:56:09 -07:00
Hanzo AI 481add2183 dispatch: retry enqueue across the cloud tasks-backend deploy gap
The render queue (/v1/tasks/*) is served by the single-pod cloud binary (Recreate
on an RWO SQLite volume), so every cloud deploy leaves a ~30s window where Traefik
has zero Ready endpoints and returns 503 'no available server' for the enqueue —
surfacing as 'GPU render worker is momentarily unavailable'. Retry the enqueue
across that transient gap (503 / connection-refused only — the request never
reached the backend, so re-sending the idempotent activityId can't double-enqueue;
a 4xx or timeout still falls straight through). ~32s ladder covers a same-node
Recreate. 3 tests: retries-through-503, no-retry-4xx, no-retry-timeout.
2026-07-21 15:51:14 -07:00
Hanzo AI 537149fd36 studio: rate/comment from the full-image view + archive trains the model
Two flywheel surfaces the gallery already had but the asset views did not.

1) Full-image (zoom) rating bar. Opening any asset full-size now shows the same
   👍/👎/💬 controls as the cards, bound to the asset in view (ZPATH threaded
   through every zoom entry — grid, queue list, stack view, openLibItem). 👍/👎
   toggle + persist via the existing castVote; 💬 opens the existing critique
   chat. Note-less; reuses the proven .vote classes. Verified on desktop + mobile
   (Playwright): centred pill, correct up/down highlight toggle, critique dialog
   opens, 44px tap targets, safe-area bottom inset, Comment label no-wrap.

2) Curation IS training signal. Archiving or flagging an output (the ONE
   /v1/library/status route every archive path funnels through — queue Archive,
   gallery delete, stack delete) now records it as a NEGATIVE example: vote -1 in
   ratings.json + an event in feedback.jsonl carrying the deleted/flagged status
   (negative-only downstream). Restoring clears that auto-negative. The downvote
   is NOTE-LESS so it trains the reward dataset without polluting prompt lessons
   (_mistakes gates on note text). A judgement the user typed is user-owned —
   never overwritten or cleared by the auto-negative. Automatic dedup writes the
   library directly (not via the route), so byte-identical twins are never
   mislabelled bad. The queue button is now an explicit "🗑 Archive" and says it
   teaches the model.

Decomplect: one atomic ratings writer (_rate_write, unique-temp — kills the
shared-name clobber the fixed ratings.json.tmp had, same hazard fixed for
library.json) shared by the rate route and the archive hook. Tests: archive →
vote -1 + feedback event + status=deleted + no injected lesson; restore clears
it; a user's downvote+note survives archive/restore and stays a lesson. 268
studio_home + middleware tests pass.
2026-07-19 15:07:45 -07:00
Hanzo AI 2f73744113 studio: content-address staged edit sources + lifecycle trace (state isolation)
Trace of the whole Edit lifecycle (click -> /v1/library/fix -> dispatch_fix ->
_fix_graph -> gpu_dispatch -> worker LoadImage) confirmed the selected asset is
bound explicitly at every hop and the low-denoise Edit graph is guarded by
_assert_edit_graph (no EmptyLatentImage / text-to-image fallback). The one
residual state-isolation hole was the staged source FILENAME.

Every lane (fix/video/model3d/compose/amend) staged its source under
    f"{kind}_src_{abs(hash((str(p), mtime))) % 10**10}.png"
— Python's process-salted hash, duplicated in 5 places. Two concurrent edits of
DIFFERENT sources whose names collided on a shared worker input dir would
overwrite each other's LoadImage file: the "Edit produced an unrelated picture /
mixed a different image" class of bug.

Decomplect to ONE helper, _stage_source(): the staged name is the sha256 of the
source bytes. The name IS a verifiable checksum of the exact selected image, so
the bytes the worker loads can never drift to another asset, and different
sources are cryptographically un-collidable (same source -> one idempotent
file). 5 call sites -> 1; the 3D downsize rides an optional max_px.

Observability the report asked for: a _trace() line correlated by job id at
build -> queue -> gpu.publish (studio.home + studio.gpu_dispatch). Logs ids,
content-address checksums, counts, lane/node ONLY — never signed URLs, tokens,
cookies or image bytes.

Tests (pure/fs, no torch/GPU/net): content-addressing (same bytes->same name,
one byte flips it, name==sha256); two concurrent edits share no staged file /
LoadImage ref / positive prompt / latent / output prefix; _collect_input_images
ships the selected org's exact bytes and is org-scoped; _assert_edit_graph
rejects a text-to-image graph. 188 middleware tests pass.
2026-07-19 13:46:12 -07:00
Hanzo AI b6f6186a39 studio: update the middleware_test fix-graph tests to the low-denoise Edit design
The CI 'Test (per hanzo.yml)' step failed because tests-unit/middleware_test/
studio_home_fix_test.py still asserted the OLD fix behavior (denoise 1.0, the
'same model, pose, framing' prompt, and the removed size=→EmptySD3LatentImage repose
branch). Updated to the new invariants: denoise == EDIT_DENOISE_DEFAULT (0.25), the
new preservation prompt, and — replacing the repose-Empty test — a
test_fix_graph_never_uses_empty_latent that proves Edit NEVER falls back to an empty
latent for any instruction (repose is now Regenerate, not Edit). Full CI set: 375 pass.
2026-07-19 13:26:00 -07:00
Hanzo AI 3e50f9414e studio: generated-image Edit is a low-denoise edit of the SOURCE, never a regenerate
ROOT CAUSE: 'Fix' on a generated gallery image ran the Qwen edit at denoise 1.0 with a
RANDOM seed (full re-paint → color/composition/subject drift even for 'brighten very
slightly'), and a broad _REPOSE keyword regex silently swapped in EmptySD3LatentImage
(a literal text-to-image regenerate) on words like 'standing'/'looking'/'side view'.

THREE DISTINCT ACTIONS now, no ambiguity:
- Regenerate = Re-run (/v1/library/rerun: re-queue the recipe, new seed) — the only
  path that may use EmptyLatentImage + denoise 1.0.
- Edit = low-denoise Qwen edit of the SOURCE: LoadImage → FluxKontextImageScale →
  VAEEncode → KSampler(latent = the source, denoise from an edit-strength slider,
  default 0.25) → VAEDecode → Save. Reuses the source's ORIGINAL seed (read from the
  embedded graph via _provenance) so a locked seed is deterministic; a varied seed
  still edits the source. NO _REPOSE→Empty branch. Full-res source (never the thumb).
- Edit selected area = masked inpaint: SetLatentNoiseMask restricts sampling to the
  painted region + ImageCompositeMasked keeps pixels OUTSIDE the mask byte-identical.

INVARIANT (impossible-by-construction): _assert_edit_graph refuses any edit whose
KSampler.latent_image is an EmptyLatentImage or whose denoise leaves the edit band
[0.05,0.95] — an Edit can NEVER silently become a Regenerate; a bad edit surfaces an
inline error, never a fallback to random generation.

Frontend: Fix dialog → 'Edit image' with an edit-strength slider (10–95% → denoise,
default 25%) + a paint-the-area mask canvas (Edit area) + clear labels (gallery card:
Regenerate · Versions · Edit). sendFix passes {strength, seed?, mask?}. Output linked
as a child (parents=[source]); color-match post-process (v0.17.9) still runs.

Tests: 6 new edit-invariant tests (VAEEncode-not-Empty, denoise 0.25, regenerate
rejected, seed determinism, prompt-preservation-not-expansion, masked composite);
existing fix test updated 1.0→0.25. 42 pass. Model-compatible with the Qwen edit model
already on spark (kept per the user's choice); I did NOT submit any live render to
verify visuals (per the no-autonomous-image-creation rule) — the graph structure is
unit-proven and the user validates the visual output.
2026-07-19 08:10:36 -07:00
Hanzo AI c3ecffd4cd studio: delete queue/history items — remove render requests you didn't want
You can now delete any queue entry (a cancelled probe, a failed attempt, clutter) —
a 🗑 on every non-active queue row + a Delete button in the item detail dialog.
worklog.remove(org, ids) + POST /v1/worklog/delete drop ONLY the log rows; output
images live in the library and are archived/deleted there (unchanged). Optimistic +
idempotent. Pairs with the earlier Queue archive (output→Archive) and the retry-cap +
dispatch-dedup that stopped the workflow from re-running/duplicating a render in the
first place. Verified: remove by id + bulk + idempotent-missing.
2026-07-19 07:44:51 -07:00
Hanzo AI 7d564eaa40 studio: gallery shows only PUBLISHED assets; ✎ Drafts chip to curate the rest
The Assets gallery (and every design filter) now shows only status=published — a
curated view. Nothing is deleted: draft + approved items stay in the library,
reachable via the new ✎ Drafts chip (count shown), where each keeps its
Approve/Publish/Fix tools so you can publish it into the gallery. Empty published
view shows a 'open Drafts to publish' hint instead of looking like work vanished.
Filter predicate verified: all→published-only, design→published-of-design,
drafts→all unpublished (excl. deleted/flagged).
2026-07-19 06:51:48 -07:00
Hanzo AI 436a965fa0 studio: thumbnails set Content-Type image/webp — fix blank grid ('assets won't load')
The WebP-thumbnail change served the .webp via FileResponse WITHOUT an explicit
Content-Type. aiohttp guesses from the extension via Python's mimetypes, which
does not know .webp, so every grid thumbnail came back application/octet-stream —
which the browser refuses to render as an <img>, so the whole gallery went blank.
The full-image path was unaffected (it sets Content-Type explicitly). Set
image/webp on the thumb response (and image/jpeg on the video poster for
consistency). Regression test pins it.
2026-07-19 06:33:17 -07:00
Hanzo AI d278c03f80 studio: org-aware empty state — 'wrong workspace' not 'my work vanished'
Root-caused 'can't see any assets': it's org resolution, not lost data. The active
org lives in the studio_active_org cookie; a fresh re-login clears it, dropping the
session to the token's home org — and if that resolves to 'default' (doesn't exist)
or an empty org (maxpower=0), the gallery is blank. The user's work is intact in
karma (416 visible) + hanzo (130). Verified the render path is sound at full scale.

Fix the confusion at the source: the empty gallery now NAMES the current workspace
and points at the org switcher (account pill, top-right) — so landing on the wrong
org reads as 'switch workspace', never 'my assets disappeared'. Also distinguishes a
no-search-match empty from a truly-empty workspace.

Verified: empty maxpower org shows 'No assets in maxpower yet — switch from the
account menu'.
2026-07-18 23:14:09 -07:00
Hanzo AI 141c4b3fc9 studio: assets/queue resilience — no silent blank, no year-long cache trap
Investigating 'can't see assets': verified the gallery renders all 416 karma cards at
full scale (1300 assets) with thumbnails painting on scroll, so the render path is
sound. Two real fragilities hardened + one cache trap removed:

- Thumb cache was 'immutable, max-age=1yr' (v0.17.17). Since the ?v=sha URL already
  busts on change, immutable bought nothing but risked locking a single bad thumb
  response in the browser for a YEAR (un-clearable without hard-refresh). → long but
  REVALIDATABLE (max-age=1wk, stale-while-revalidate), so a bad response self-heals.
- runs() did (await fetch(x)).json().catch() — catches the json reject but NOT a
  fetch reject, so a failed ratings/favorites fetch threw before rgrid rendered →
  blank queue. Now each non-critical fetch is fully guarded.
- load() now shows 'Couldn't load your library — retry' on a transient /v1/library
  failure instead of a silent blank gallery.

Happy path verified unchanged (416 cards, queue renders).
2026-07-18 17:37:21 -07:00
Hanzo AI 4b128ccfc8 studio: GPU banner never sticks on 'connect a GPU' — cache only a positive
/v1/gpu-status returns online reliably (verified live), but gpuOnline() cached
BOTH true and false for 15s. When a worker's heartbeat blipped for a few seconds
(spark flapped tonight on a local-network issue), the banner cached 'no GPU' and
held it — showing 'connect a GPU to generate' even with GPUs 3/3 in the chip.
Now only a POSITIVE is cached; a negative re-checks every call, so the banner
self-heals on the next 5s livebar tick.
2026-07-18 11:18:16 -07:00
Hanzo AI 20a433678c studio: faster + lighter image loading — lazy thumbs, WebP, immutable cache
Four wins across gallery + queue + stacks:
1. LAZY: cards no longer eager-load every thumbnail (a 1000-image gallery fired 1000
   requests up front). Each .im carries its URL as data-bg; an IntersectionObserver
   (rootMargin 600px) paints it only as the card nears the viewport → a screenful of
   requests, not N. Falls back to eager where IO is unavailable.
2. WEBP: thumbnails are now WebP q80 instead of JPEG q82 — measured 45% smaller.
3. QUEUE was loading FULL-RES outputs (vURL) as row backgrounds — now uses the same
   downsized thumbnail as the gallery (huge cut on the History tab).
4. IMMUTABLE CACHE: thumbURL is versioned by content sha (?v=, from the indexer) or
   mtime, so a thumb is served Cache-Control: immutable, max-age=1yr — instant on
   repeat loads; a re-render gets a new sha → new URL → never stale.

Verified: lazy observer paints in-view cards + defers off-screen; WebP 23KB vs JPEG
41KB on a sample; ?v= present on thumb URLs.
2026-07-18 11:08:36 -07:00
Hanzo AI c474161fba studio: Queue view — fullscreen lightbox + ←/→ nav, 👍/👎 + reject on outputs
On Queue & History, per the ask:
- Click a photo → fullscreen (already opened zoom, now with navigation). Generalized
  the lightbox: zoomList(list,i) carries the queue's outputs so the on-screen ‹ › AND
  the keyboard ← / → step through them (wraps both ways); Esc closes. Stack view still
  uses svStep — zStep() routes to whichever context is open.
- 👍/👎 + 💬 on each output — the SAME rate endpoint + feedback.jsonl training log as
  the gallery, keyed by the output's library path (a queue vote == a gallery vote).
- 🗑 Reject → Archive — soft-deletes the output (⌘Z-undoable), dims the row + marks it
  ✓ Rejected. Reuses the one status/undo path; no new lifecycle.

Each queue output resolves to its library asset (path = subfolder/filename), so it
reuses the gallery's vote/archive machinery — one way to do everything, no duplication.
Verified headless: lightbox a→b→c + wrap via ←/→ and the arrows; 👍 logs training
feedback (stats up=1); reject archives + ⌘Z toast.
2026-07-18 10:47:53 -07:00
Hanzo AI e16131b0b6 studio: manifest indexes .glb — 3D meshes survive the sweep (kind=model3d)
The build-manifest sidecar scanned only image/video/marketing extensions, so a
.glb was dropped on every rebuild — the ingested mesh vanished from the library
within a sweep cycle (same class as the earlier video/ingest drops). Add
MODEL3D_EXTS to the scan and the kind default.
2026-07-18 10:02:49 -07:00
Hanzo AI 2a5bb8a71d studio: reliable dispatch with large inputs — cap 3D ref at 1536px, raise enqueue timeout to 90s
A 4k catalog master (~20MB) inlined to the gpu-jobs enqueue overran the 15s
urlopen timeout, so dispatch_if_worker fell back and the model-less pod returned
'GPU worker momentarily unavailable' on a perfectly good render — for EVERY lane,
not just 3D. Two fixes: the 3D lane downscales its staged reference to 1536px
(Hunyuan3D works at ~518px internally, so the mesh is identical and the body is
small), and the enqueue upload timeout goes 15s→90s so large fix/compose/video
references actually finish uploading.
2026-07-18 09:59:13 -07:00
Hanzo AI 94e0c4e685 studio: job-detail UX — big result preview (image/video/3D), mobile bottom-sheet
The queue/history detail showed the render as a 60px thumbnail — you open a job
to SEE the result. Now the result is a large hero: an image (zoom on click), a
video (inline player), or an interactive rotatable <model-viewer> for a .glb.
References move below as a labelled small row. On phone every dialog becomes a
full-width bottom sheet (safe-area padding, scrollable, stretched action
buttons), the hero caps at 48vh so it fits, and queue-row thumbs grew 60→74px.
2026-07-18 09:57:46 -07:00
Hanzo AI 100ca8d8a2 studio: structural fix — duplicate renders impossible by construction
The retry-cap + dispatch-dedup patches stop the two known triggers; this makes the
whole class impossible at every layer, independent of any upstream bug:

COMPUTE — don't render a duplicate:
  * retry cap (maximumAttempts=1): a failed render never re-runs itself.
  * dispatch dedup: identical (org, prefix, instruction, refs) submits within 60s
    collapse to the in-flight render.

QUEUE — nothing lingers to replay:
  * scheduleToStartTimeout=1800s: a job no worker picks up in 30 min EXPIRES, so a
    worker outage can't leave a backlog that silently re-bills when a worker returns.

STORAGE — the gallery is CONTENT-ADDRESSED (the last line of defense):
  * library_manifest (the one indexer that builds every library.json, sweeping each
    org continuously) now enforces AT MOST ONE active asset per content hash. Any
    byte-identical duplicate is auto-soft-deleted, keeper = a curated asset if one
    exists else the earliest. Idempotent, recoverable, and O(new-files) per sweep
    (sha reused when mtime is stable). So even 873 identical re-renders can NEVER
    accumulate in the gallery — the invariant heals it within one sweep.

Verified: 3 identical + 1 distinct -> 2 active (one-per-hash), distinct untouched,
stable across sweeps.
2026-07-18 09:56:13 -07:00
Hanzo AI c088a3833c studio: 3D lane — image→.glb mesh (Hunyuan3D-2.1) over the shared library/queue/flywheel
Third suite content type. Proven node-for-node on spark from ComfyUI's own
3d_hunyuan3d-v2.1 template: ImageOnlyCheckpointLoader (the ONE bundled
model+CLIP-vision+VAE — separate loaders produced noise) → CLIPVisionEncode →
Hunyuan3Dv2Conditioning → ModelSamplingAuraFlow(shift 1, the flow-matching
wrapper WITHOUT which the sampler yields fragments) → KSampler → VAEDecodeHunyuan3D
→ VoxelToMesh(surface net) → SaveGLB. Verified: a clean 3D garment mesh from a
ghost-mannequin shot (341k faces, coherent).

Lane contract, same as video: _model3d_graph + POST /v1/library/model3d (ONE
reference image required, no text-to-3D, unique job tag); .glb ingest (glTF
magic bytes, model/gltf-binary CTYPE, kind=model3d); an interactive <model-viewer>
zoom card (grid shows a 🧊 3D badge, click to rotate); the 'Image → 3D' template.
No new infra — one library, one queue, one wallet, one GPU fleet.

Tests: proven graph, dispatch route (org-from-token, unique tag, one required
ref), .glb ingest. All studio suites green.
2026-07-18 09:27:33 -07:00
Hanzo AI 1671b21334 studio: video lane verify-fixes — reset composer lane after video; one saver list
Adversarial review (contract lens) blocked on a real bug: the composer KIND flag
stayed 'video' after a video dispatch, so the next single-photo generate silently
POSTed /v1/library/video (image-to-video) instead of /v1/library/fix — a wrong,
billable GPU dispatch. clearLane() now resets after every video dispatch, and the
Template chip is clickable to return to the neutral edit/compose lane.

DRY (identity lens): the SaveImage/SaveVideo/SaveWEBM triple was hardcoded three
times. Consolidated the two Python copies into gpu_dispatch.SAVE_NODES (the base
module, no cycle) imported by studio_home; the HTML keeps its own JS-runtime copy.

244 pass; XSS esc() + job-identity _retag + library.json lock all intact post-rebase.
2026-07-18 02:07:10 -07:00
Hanzo AI 193aee3aac studio: fix the library.json curation-wipe race + make /ready check worker liveness (Red HIGH x2)
LIBRARY.JSON CORRUPTION (Red V3, the curation-wipe mechanism): the build-manifest
sidecar wrote a FIXED '.library.json.tmp', shared by every pod's sidecar — two
overlapping during a roll interleaved into one inode → torn JSON → load_prior()/
reader hit JSONDecodeError → {} → every asset reset to status=draft, which mirror
then pushed to S3 and restore rehydrated. Fixed both writers: mkstemp gives each a
private tmp (kills the tear), and a shared flock (.library.lock, taken by the
sidecar across build+write and by the app's _save_library across its write)
serializes the two rebuild-then-replace paths so neither clobbers the other.

/READY LIVENESS (Red V1): the readiness probe checked ,
set once at boot and never cleared — constant-true, saying nothing about the render
path. A dead worker thread kept the pod in-rotation accepting jobs that never ran,
and a roll would promote a wedged pod. /ready now gates on the prompt_worker daemon
thread actually being alive.

Tests: unique-tmp, no fixed-tmp residue, concurrent-writers-never-tear, lock file.
226 pass.
2026-07-18 01:27:54 -07:00
Hanzo AI 9370bf82b2 studio: stop 'the same reject over and over' — dedup dispatch + cap retries
Two root causes of repeated/duplicate GPU renders (money + gallery clutter):

1. NO retry cap: the render activity had heartbeat/startToClose timeouts but no
   retryPolicy, so the tasks framework re-ran a failed render (e.g. spark OOM mid-
   sample) — the same reject re-rendered. Add retryPolicy maximumAttempts=1: one
   attempt, a failure surfaces to the user, no auto re-burn.

2. NO dispatch idempotency: a double-fired/unguarded submit queued a SECOND render
   of the same thing (logs showed one output dispatched 4x in a second). _dispatch
   is now idempotent — an identical (org, output-prefix, instruction, refs) dispatch
   within a 60s window returns the in-flight prompt_id instead of billing a new job.
   A genuinely different instruction on the same source still dispatches.

Server-side, so it covers every submit path (composer, fix dialog, compose, chat).
Verified: 4 identical submits → 1 render; different instruction → allowed; retry
cap present.
2026-07-18 01:07:36 -07:00
Hanzo AI 9d18ad1c75 studio: unique job identity for re-run + template-render; composer double-submit latch
Red-team, same phantom-image class the user hit: re-run replayed the source
graph's own SaveImage prefix, so its file landed in the SOURCE namespace and N
re-runs shared one prefix — the library then showed the newest sibling for every
row (a job displaying a photo it never made). _retag() now points every SaveImage
at a unique per-dispatch prefix for re-run and template-render, the guarantee
fix/compose/amend already had.

Composer create() (↑ button / Enter) had no double-submit guard — a fast double
fired two billable dispatches. Now wrapped in the same enqueue()/QBUSY latch as
the dialog buttons. 223 pass.
2026-07-18 00:39:24 -07:00
Hanzo AI 6fcd119bc8 studio: per-image critique chat + archive from queue + stored-XSS fixes
CRITIQUE CHAT (the ask: chat about a generated photo, to make it better and
train it): the 💬 on any render now opens a conversation ABOUT that image.
/v1/library/critique loads the actual photo so the vision model's reply is
grounded in what was rendered, and EVERY user turn is recorded as the note the
flywheel reads (_mistakes → _lessons), so critiquing here conditions the next
generation of that design. The old sidebar copilot stays graph-only; this is
the image-scoped path. Thread persists per image (reopen to continue).

ARCHIVE FROM QUEUE: done rows in Queue & History get a 🗄 button — archive the
render straight from history (soft, ⌘Z to undo), no need to find it in the grid.

STORED XSS (red-team, launch-blocking): a hostile upload filename like
'<img onerror=...>.png' rendered raw into gallery card HTML and executed for any
org member. Fixed at the boundary (upload name collapsed to [A-Za-z0-9._-]) and
in depth (esc() on the filename text in render(); template Render onclick now
carries only the safe slug, name looked up client-side). svCard already esc'd —
render() and the template card were the gaps.

Tests: critique thread persistence + lesson recording + tenancy + guards;
upload-name sanitization. 222 pass.
2026-07-18 00:36:45 -07:00
Hanzo AI 5ccee8f052 gate: survive reasoning judges — verdict from reasoning_content, last-token rule, unstarved budget
zen-vl became a reasoning model: max_tokens 8 was spent entirely on
reasoning_content, content came back null, the strict parse read nothing and
the gate failed open on EVERY render — flagged reached zero while corrupted
outputs sat in the grid, and the failure logged at an invisible level.

_reply_text now falls back to reasoning_content when content is empty;
max_tokens 768; the verdict is the LAST whole PASS/FAIL token (deliberation
mentions both — position is the conclusion); judge failures log at warning.
2026-07-17 23:10:10 -07:00
Hanzo AI 7d419c2101 studio: rebrand the Editor frontend → Hanzo Studio (no ComfyUI in the UI)
The Advanced-mode node editor is served from the upstream comfyui-frontend package,
whose index still shipped <title>ComfyUI</title> + 'Loading ComfyUI...' — the only
user-facing surface still showing the upstream name (the home + web/dist SPA are
already Hanzo Studio, and the running image is our fork ghcr.io/hanzoai/studio).

editor_index() already injects our shell into that index; add two idempotent swaps
there so the Editor tab title + loading status read 'Hanzo Studio'. The ComfyUI-format
compatibility stays internal, per rule — the name is out of the UI. (Deeper editor
menu/i18n strings live in the minified frontend bundle; a full frontend rebrand is a
separate build-side change.)
2026-07-17 22:55:35 -07:00
Hanzo AI 5087403496 studio: launch batch — product analytics + UX polish + dispatch-test reconcile
Usage tracking: Umami pageviews (analytics.hanzo.ai, pre-existing site id) and
six product events (generate, fix, compose, stack_create, rate, template_use)
via a zero-dependency sendBeacon to insights.hanzo.ai/v1/e — the /v1 prefix is
load-bearing (root /e/ is IAM-gated) and the key is the project's public write
token. Telemetry fails silent.

UX: body clears the floating shell dock (content was hidden under it at page
bottom on phone); stack name input flexes instead of truncating.

Tests: gpu_dispatch tests updated to the has_models_for(prompt) seam
(per-graph model check renamed the old has_local_models).
2026-07-17 22:39:37 -07:00
Hanzo AI d6b888dbd0 studio: ingest owns its dispatch row — prefix-aware has_output kills the duplicate history rows every mirrored render created 2026-07-17 22:18:02 -07:00
Hanzo AI 225658f60f studio: kill 'Prompt outputs failed validation' for real — per-graph model check
Reported again on compose (base + ref). Root cause the v0.17.5 guard missed: the
/prompt intercept fell back to LOCAL validate_prompt whenever dispatch didn't happen
AND has_local_models() was True — but has_local_models() is 'ANY model present', and
the coordinator pod carries a stray SD1.5 checkpoint. So a Qwen fix/compose graph the
pod CANNOT load (no qwen-image-edit unet) still validated locally → 'unet not in []'
→ 'Prompt outputs failed validation'. The stray checkpoint defeated the model-less
invariant.

Fix: replace has_local_models() with has_models_for(prompt) — a per-GRAPH check of
the UNET/checkpoint/CLIP/VAE loader inputs against folder_paths. A box renders locally
only a graph it can actually load; every other graph dispatches to the fleet (or a
retryable 503), regardless of unrelated models on disk. Used in BOTH the dispatch
decision (gpu_dispatch.dispatch_if_worker) and the intercept fallback (server.post_prompt).

Verified: stray-SD1.5 pod → False (enqueue, not local); real Qwen box → True (renders
locally, dev/worker unaffected); model-less pod → False.
2026-07-17 22:17:31 -07:00
Hanzo AI d561e35584 studio: fix the top composer — one create box, ↑ actually generates
Reported: adding a photo to the top 'What should we create?' box opened a SECOND
box at the bottom; only that bottom box worked; typing in the top box + clicking ↑
did nothing.

Root cause: the top ↑ (create()) was a STUB that only toast'd a HIP-0506 placeholder
and never generated, while the '+' funnelled photos into a separate fixed bottom
'Next generation' bar (#reftray/genFromTray) that was the only real generate path.
Two composers, only the second wired.

Fix — collapse to ONE composer (the top box):
- create() now really generates from the top prompt + the photos attached to it:
  1 photo → edit (/v1/library/fix), 2-5 → compose — the same proven graphs the
  gallery uses. Text-only (no photo) gives a clear 'attach a photo with +' hint.
- attached photos render INSIDE the top composer (a thumbnail strip with × to
  remove), fed by the same TRAY (+ / drag / paste / a gallery card's +).
- the bottom #reftray bar + genFromTray are removed (dead once create() is real).
- Enter submits (Shift+Enter = newline).

Verified headless: attach → thumb in the top box, NO bottom bar; type + ↑ → POST
/v1/library/fix {upload, instruction} → Queued.
2026-07-17 22:17:31 -07:00
Hanzo AI d3c5dca63f studio: repose graph mirrors the proven manual family — raw reference into the encoders, no CFGNorm on the free-latent path
Scaling the reference against a differently-sized empty latent re-created the
size-mismatch artifact class (oversaturated drift + reinvented garment,
observed live). Raw ref + AuraFlow-only matches the graphs that delivered the
velvet-front and mesh-back renders. Known limit, verified across two families
and three prompts: a true 90-degree profile from a front-facing photo
reference is beyond the current model — back views and three-quarter turns
render correctly.
2026-07-17 20:58:57 -07:00
Hanzo AI 9062c238e8 studio: the queue detail shows which notes conditioned the job — 'Learning from your notes' from worklog params 2026-07-17 20:24:02 -07:00
Hanzo AI b81974b52f studio: flywheel closes at dispatch — every pointed-out mistake conditions the next generation
Rating an output (👍/👎/💬 on every card) already logs to the per-org training
dataset. Now the retrieval half: at fix/compose/amend dispatch, the mistakes the
user noted on the base and its lineage (👎 or ≤2★ notes, and bare notes) are
injected into BOTH prompt encoders — an avoid clause in the positive, the texts
in the negative — so the very next render on that design cannot repeat what was
pointed out. Praise notes are never injected. The same judgements keep
accumulating in feedback.jsonl (with curation status) for weight training.
2026-07-17 19:21:38 -07:00
Hanzo AI 1c7d66ce34 studio: one output resolution for every view — lineage can no longer attribute a pending job's prefix-matched sibling as a version 2026-07-17 19:16:09 -07:00
Hanzo AI 76af4f30e8 studio: unique output prefix per dispatch + done-only result display — a queued job can never show or claim an earlier run's rejected file
The queue detail showed a prior (rejected) render as a queued job's result:
re-fixing the same source reused the same SaveImage prefix, and every view
prefix-globbed disk regardless of row status. Each dispatch now appends a
6-hex job tag to its prefix (fix/compose/amend), rows expose an output only
once THEY are done, and the workflow route runs the same finalize mechanism.

Also routes view/pose-change instructions (from the side, back view, turn
around…) to the free-latent edit variant: the anchored fix latent encodes
the base — deliberately structure-preserving — and its prompt tail pinned
'same pose, framing', which contradicted exactly these asks. Repose asks now
get EmptySD3LatentImage at the reference's aspect and a tail that frees the
pose while keeping model/outfit/hair/lighting.

Feedback records now carry the judged output's curation status, so a
flagged/archived output is negative-only signal downstream — never a
positive training exemplar.
2026-07-17 19:14:46 -07:00
Hanzo AI 701d166b3f studio: vote + text feedback on renders → training dataset (flywheel)
There was no way to turn user judgement into a training signal — ratings.json fed
only the runtime copilot, on the runs view, as stars. This adds a clear 👍/👎 vote
+ a written note on EVERY render (gallery card + a feedback dialog), and logs each
judgement, immutable, to the org's append-only training dataset.

- 👍/👎 on each image card (one click; click again to clear) + 💬 opens a dialog
  for the written critique ('what should the model do differently?').
- /v1/library/rate extended with a normalized vote (1|-1|0); every rate also
  appends {ts,user,org,key,output,vote,stars,text,prov} to orgs/<org>/output/
  feedback.jsonl — the flywheel the model trainer consumes. The output PNG each row
  anchors to carries its own prompt+refs in its embedded graph, so the row stays
  small and the dataset is training-ready (preference / reward / instruction-tuning).
- GET /v1/feedback/stats (👍/👎/with-text/total) + GET /v1/feedback/export (the JSONL,
  as a download). Toolbar shows '⬇ Feedback (N)'.

ratings.json stays the current-state store the UI reads; feedback.jsonl is the
append-only event log — orthogonal concerns, one rate call writes both. Verified
end-to-end headless: vote registers + stats increment, dialog captures vote+text,
export emits training-ready JSONL; backend unit + full app reboot clean.
2026-07-17 19:01:48 -07:00
Hanzo AI 49c4f28e76 studio: serve webp with its real content type — attached webp references render everywhere 2026-07-17 18:58:14 -07:00
Hanzo AI 21d25458b9 studio: double-queue guard — queue buttons react instantly and latch until the enqueue resolves 2026-07-17 18:58:14 -07:00
Hanzo AI 0fd473923b studio: color-fidelity for Fix outputs — kill the fix-of-a-fix color drift
A Qwen-Image-Edit fix runs at denoise 1.0 → it re-paints EVERY pixel, so the
render's global white-balance/exposure/saturation drift off the source, and the
drift COMPOUNDS across fix-of-a-fix (each fix re-encodes the prior, already-shifted
output). That is why repeatedly-fixed images end up visibly mis-colored.

Fix: restore the render's global color statistics to its source (Reinhard mean/std
transfer, per RGB channel) — the local edit stays, the global cast goes; a render
that didn't drift matches to itself = untouched. Verified: 4 successive fixes hold
Δ→source ~1 with the match vs 6.5→16.5 (compounding) without.

Applied POD-SIDE at worker-render ingest (server.upload_output → colormatch_fix_ingest):
- needs NO node on the GPU worker and touches no render graph → can never make a
  render fail (avoids the 'failed validation' class entirely)
- scoped to fixes/… outputs only (never compose/normal renders), keyed to the
  staged source via the render-jobs record
- fail-safe throughout: any error / missing source / size mismatch leaves the raw
  bytes exactly as uploaded (proven: a save-format bug left files untouched, never
  corrupted)
- strength 0.85 default (STUDIO_COLORMATCH_STRENGTH), leaving headroom for an
  intentional global recolor in the instruction

New middleware/color_fidelity.py (pure algorithm + atomic in-place match);
studio_home.colormatch_fix_ingest + _fix_source_ref resolve org+source.
2026-07-17 18:11:56 -07:00
Hanzo AI 4ac849c28e studio: select circle sits below the archive x — the two corner controls no longer overlap 2026-07-17 17:40:58 -07:00
Hanzo AI 2ee53f67e4 studio: select circle always visible on every card (ghost at rest, solid on hover/selection) 2026-07-17 17:18:15 -07:00
Hanzo AI 5016d2c5d8 studio: rename Deleted → Archive + permanent Delete forever
The soft-delete holding area is now an Archive (recover any item, or the
whole history stays visible). Inside it, permanent removal:

- 🗑 Delete forever per card + bulk on a selection, confirmed via the
  existing <dialog> ask() (irreversible — no ⌘Z can undo an unlinked file)
- 🗑 Empty Archive clears the lot in one confirmed step
- POST /v1/library/purge {paths[]|all}: unlink file + drop record + drop
  cached thumb. Archive-only by design — refuses any asset whose status is
  not 'deleted', so a gallery mis-click can only ever archive (recoverable),
  never destroy. Verified end-to-end headless: per-card/bulk/empty purge,
  the archive-only guard, and rename coverage (tab, count, chips, toasts).

Rename is display-only; the underlying status stays 'deleted', so the
1,259 already-archived items need no migration.
2026-07-17 17:10:21 -07:00
Hanzo AI 7ba4956e8a studio: select any image directly — circle on every card enters selection; group delete / stack / move 2026-07-17 16:45:09 -07:00
Hanzo AI 8423eada67 studio: in-page ask dialog — suppressed native prompt()/confirm() silently killed Stack create/rename/tag/delete confirms 2026-07-17 16:43:47 -07:00
Hanzo AI 4ee5adcef9 studio: Dedupe — one-click removal of exact-duplicate images (recoverable, ⌘Z)
The catalog seeder emits byte-identical copies (found 94 dups in one org's 268
visible assets). POST /v1/library/dedup hashes visible assets and soft-deletes
exact-content duplicates — keeps one per cluster, distinct re-renders (different
seeds) untouched — and returns the moved paths. Frontend "Dedupe" button beside
Select; the whole sweep pushes ONE undo entry so ⌘Z restores it all. no-store on
the response. Verified in-browser: dedup hides dups, Deleted count updates, ⌘Z
restores. v0.17.7
2026-07-17 16:19:13 -07:00
Hanzo AI c86ded1bad studio delete UX: ⌘Z undo, findable Deleted tab, one-click bulk delete + no-store library
Deletes already persist server-side (verified live: soft-delete → status 'deleted'
holds, zero reverts); the "moved to deleted then restored" was the frontend serving
a cached /v1/library on refresh. Fixes:
- /v1/library → Cache-Control: no-store, so a delete is never masked by a stale copy.
- ⌘Z / Ctrl+Z undoes the last delete (and any status change) — soft-deletes are
  reversible, so undo is just a status write-back. Dropped the bulk confirm dialog.
- 🗑 Deleted is now a top-level tab with a live count — trash is easy to find;
  per-item Recover + bulk Recover (context-aware bulk bar).
- Bulk: Select → pick many → one-click "🗑 Delete selected". Discoverable.
Verified in-browser (Playwright): delete hides the card, ⌘Z restores it, Deleted tab
filters + Recover works. v0.17.6
2026-07-17 16:09:39 -07:00
Hanzo AI a363751352 studio: dispatch resilience — kill the intermittent "failed validation" on fix
The /prompt intercept sends every render to the org's GPU worker, but a
transient fleet-check fault (/v1/machines 502 / TLS reset / timeout) made
dispatch_if_worker return False, so the render fell through to LOCAL
validation on the model-less cloud pod → the cryptic "Prompt outputs failed
validation" (unet_name '…' not in []). That's why Fix failed only *sometimes*.

- _fleet_machines: retry 3x with backoff so a transient hiccup isn't read as
  "no GPU" and bounced onto the model-less pod.
- has_local_models() + intercept: on a model-less coordinator pod a failed
  dispatch now returns a clear retryable 503 "GPU render worker is momentarily
  unavailable — please retry", never the doomed local validation. A box WITH
  models still renders locally (dev). v0.17.5
2026-07-17 15:17:26 -07:00
Hanzo AI 86e301345f studio /studio: serve the rich home (gallery remove/edit/+ refs), not the minimal SPA
The web/dist chat SPA shadowed studio_home.html at /studio, dropping the
per-image affordances: delx (remove → POST /v1/library/status deleted),
the Fix dialog (type-and-edit → /v1/library/fix), and the + reference tray
(→ /upload/image → /v1/library/compose, up to 5 refs). Serve the rich home
explicitly; drop the dead /studio/assets route + the node web-build stage.
web/login.html still ships (server.py serves /login). v0.17.4
2026-07-17 13:06:01 -07:00
Hanzo AI f75cb4b0cc studio-chat: keep renders reachable on mobile (library becomes a bottom strip)
On a phone (<=820px) the library rail was display:none, so a user could chat and
dispatch renders but never see them. Now the body stacks (chat fills, library
drops under it) and the rail shows as a compact scrollable bottom strip — but only
when there is something to show (jobs or assets set the `active` class), so the
empty chat state stays clean. Verified with Playwright at iPhone 14 (390x844):
empty = clean chat; with renders = 4-col strip under the composer, no overflow.
2026-07-17 09:38:42 -07:00
Hanzo AI 2bf5421313 studio: serve the /v1/agent chat SPA (web/dist) at /studio
The studio-chat frontend (web/, Vite on @hanzo/ai talking to POST /v1/agent) was
repointed + committed but never served. Build it in a node stage of the Dockerfile
(-> /app/web/dist) and serve it at GET /studio, with GET /studio/assets/{name} for
the hashed bundle (immutable) and a traversal guard. Falls back to the legacy
studio_home.html when the build is absent so /studio is never a dark page. This is
the decided cutover: studio.hanzo.ai/studio becomes the /v1/agent chat app.
2026-07-17 01:13:51 -07:00
Hanzo AI dd1f9e7138 studio-chat: call /v1/agent (was /v1/chat), canonical preset field + conversation threading
The orchestrator lives at POST /v1/agent (github.com/hanzoai/agent in the cloud
binary), not /v1/chat (which ai owns as its completions route). Use the canonical
`preset` selector (not the legacy `capability` alias) and thread conversationId so
the per-org history the orchestrator persists actually continues across turns.
VITE_CHAT_API → VITE_AGENT_API.
2026-07-16 21:10:06 -07:00
Hanzo AI a9223e59e8 studio/web: call api.hanzo.ai/v1/chat directly — no studio proxy layer
Chat hits the cloud clients/chat orchestrator directly (credentials:'include'
carries the .hanzo.ai cookie cross-origin; handler replays it into the billed
completion). Engine calls stay same-origin. Needs gateway CORS to allow the
studio.hanzo.ai origin with credentials — infra config, no backend code.
2026-07-16 10:57:33 -07:00
Hanzo AI 10eec303fd studio: web/ — our unique product frontend (chat app on the shared Hanzo stack)
Vite+React studio-chat: say what to create, the cloud /v1/chat orchestrator drives
studio's render tools, outputs land in the library. Increment 1 (own minimal UI
wired to /v1/chat create + library/queue/gpu). Served at /studio; the ComfyUI node
editor stays separate in studio-ui (was studio-frontend). Builds clean (npm-published
@hanzo/*).
2026-07-16 09:48:39 -07:00
Hanzo AI b07530398d studio: /v1/mcp render tools (fix/compose/ask) for the cloud chat tool registry
MCP-over-HTTP (JSON-RPC 2.0) exposing the studio render pipeline as tools the
cloud /v1/chat 'create' capability drives via the tool registry. Reuses the ONE
dispatch path (studio_home.dispatch_fix/dispatch_compose, extracted from the
/v1/library/{fix,compose} routes) so an LLM-issued fix behaves identically to a
UI one. Registered per-org via cloud POST /v1/tools/servers. v0.17.3.
2026-07-16 09:01:36 -07:00
Hanzo AI 0d307dceb2 studio: responsive — home was desktop-only (0 media queries). Add tablet(≤900) + phone(≤600) breakpoints: fluid grid, wrapping controls, full-width chat sheet, bigger tap targets, always-show card tools on touch (no hover-reveal). Works mobile/tablet/laptop/desktop. v0.17.2 2026-07-16 00:21:55 -07:00
Hanzo AI 1b713f904c studio: fix queue jobs VANISHING after 30s — render-queue completion check did a fuzzy base-prefix match that collided with the SOURCE image (a fix's output prefix derives from the source name), so every job looked 'done' after 30s and disappeared. Now checks for the EXACT output file (<outPrefix>_NNNNN_.png). This is why 'the queue is not working'. v0.17.1 2026-07-15 23:19:39 -07:00
Hanzo AI c969641d7d studio: EXIT Advanced mode — always-visible '← Studio' button (fixed top-left) + 'Exit to Studio Home' in the user menu. Advanced mode (the full-screen node editor) had NO way back to Studio Home — users got stuck. v0.17.0 2026-07-15 23:11:34 -07:00
Hanzo AI b7dff20861 studio: fix 'connect a GPU' when a GPU IS connected — UI checked /v1/engines (worker-federation) but 'hanzo gpu connect' registers with the FLEET (gpu-jobs), which is what dispatch actually uses. New /v1/gpu-status reports the SAME source as dispatch (_has_online_gpu → /v1/machines); UI now shows spark connected. v0.16.9 2026-07-15 22:58:04 -07:00
Hanzo AI e696fb7480 studio: SHOW BYO-GPU renders in the queue — fix/compose dispatch to gpu-jobs (worker), NOT the pod's local /queue, so a queued fix was invisible ('didn't bring it into queue'). Now dispatch_if_worker tracks each job per-org; /v1/render-queue serves in-flight ones (drops on output-landed/age-out); live-bar + Queue tab show them with thumbnail + ETA. v0.16.8 2026-07-15 22:20:50 -07:00
Hanzo AI b3f20495f8 studio: /studio served no-store — browsers cached the SPA HTML and served a STALE page after deploys, making UI fixes (delete) look broken to users on yesterday's cached page. v0.16.7 2026-07-15 22:12:29 -07:00
Hanzo AI 179b577add studio SECURITY: org-scope the inherited core endpoints (multi-tenant release blockers from red audit) — /history + /history/{id} filtered by caller org (was cross-tenant BOLA, live in Queue&History UI); /queue GET filtered; /queue clear/delete scoped (was: any tenant's 'Clear all' wiped ALL orgs); /interrupt scoped to caller's running job; org_id path-traversal sanitization in get_org_base_path. Single-tenant (auth off) unchanged. v0.16.6 2026-07-15 21:37:33 -07:00
Hanzo AI c2336e0c1e studio: YouTube-style up-next queue rows — reference thumbnail + title + brief description (the prompt) + ETA/#-in-line; serves staged fix/compose refs via ?input=1; v0.16.5 2026-07-15 20:51:01 -07:00
Hanzo AI 0538abf199 studio: FAST thumbnails (?thumb=1 → cached 480px JPEG, fixes slow full-res grid loading) + always-visible instant 🗑 trash on every card (optimistic delete, feels instant) + tray/compose use thumbs; v0.16.4 2026-07-15 20:49:03 -07:00
Hanzo AI 7eefdcd206 studio: persistent live-queue bar (always visible, non-advanced) — shows what's Generating now + queued count + ETA, polls /queue every 5s; auto-hiding header + full-bleed immersive stage CSS + Hanzo AI chat widget scaffold; v0.16.3 2026-07-15 20:42:01 -07:00
Hanzo AI b238b8c568 studio: bump BYO-GPU render deadline 600s→1200s (heavy Qwen edit on cold GB10 exceeds 10min); v0.16.2 2026-07-15 20:18:30 -07:00
Hanzo AI e97194613c studio: '+' on each photo → reference tray for the next generation; 1 ref=fix/edit, 2-5=compose; describe + Generate from the floating tray; v0.16.1 2026-07-15 20:12:56 -07:00
Hanzo AI b31a1ea3e2 studio: YouTube-style Queue & History — Now-running + Up-next (live /queue, remove/clear) + History with 0-5 star ratings + notes-for-AI (real /v1/library/rate + ratings.json curation store) + favorite→save-as-template guided flow; v0.16.0 2026-07-15 19:58:21 -07:00
Hanzo AI dd21fd9f85 studio: multi-asset Compose (select N → describe how to combine → /v1/library/compose, multi-ref Qwen graph) + Fix/Compose now report GPU-online status clearly (Qwen models live on BYO-GPU); v0.15.9 2026-07-15 19:50:41 -07:00
Hanzo AI 109cd04a54 studio: soft-delete (recoverable, Deleted filter) + multi-select bulk (approve/publish/fork/delete) + hover-reveal tools (cleaner grid) + systematized fashion templates (CAD→ghost, outfit-switcher, product, lifestyle, CAD→3D grouped by category); v0.15.8 2026-07-15 19:22:32 -07:00
Hanzo AI 552f47b844 studio: chat-first Claude-Design home ('What should we create?' + full Hanzo template set: product/ghost/CAD-3D/video/music/voice/social/docs/paper) + org-scoped /v1/library/file (fixes blank thumbnails); v0.15.7 2026-07-15 18:33:59 -07:00
Hanzo AI b0d1b06a6f studio home: Hanzo logo top-left + clean inline org switcher/avatar (drop broken floating identity pill); v0.15.6 2026-07-15 18:30:09 -07:00
Hanzo AI 023367bfec studio home: monochrome brand (drop yellow --brand) + surface org-switcher/user/wallet identity pill; v0.15.5 2026-07-15 17:42:05 -07:00
Hanzo AI b2121abc71 chore: version 0.15.4 — rebuild Studio Home (0.15.3 image never reached GHCR) 2026-07-14 17:51:53 -07:00
Hanzo AI acd3bcc99c chore: version 0.15.1 — deep-link opener atop the live 0.15.0 line 2026-07-14 06:32:07 -07:00
Hanzo AI 2ea07a2678 feat(studio): deep-link opener — ?org=&workflow=&run=1 opens (and runs) a saved workflow
A gallery/console/chat link lands you on the graph ready to render: sets the
session org (so a re-run dispatches to that org's BYO-GPU and outputs to its
gallery), loads the saved workflow, and optionally auto-queues. Injected as a
frontend shim (patch_deeplink.py, step 13 of apply-branding), guarded end-to-end.
2026-07-14 06:06:24 -07:00
Hanzo AI 1dba6abcbf fix(ci): launch smoke test waits 120s — boot now includes alembic + assets scan, 30s flakes at the boundary 2026-07-12 22:53:35 -07:00
Hanzo AI cc352ba658 chore: version 0.14.11 2026-07-12 22:50:42 -07:00
Hanzo AI 3bb37419b5 chore: version 0.14.10 2026-07-12 22:50:16 -07:00
Hanzo AI 5c9215afe3 fix(cloud): persist models on the tenant-data PVC
/app/models is ephemeral overlay — every model vanished on pod restart
(CheckpointLoaderSimple: 'not in []'). Bake extra_model_paths.yaml into
the image registering /app/orgs/.models (PVC) as the default model store
for all model classes; main.py auto-loads it. Downloads land durable.
2026-07-12 22:42:34 -07:00
Hanzo AI 82a8487e7b fix(test): force CPU device path before importing execution — GPU-less CI runner has no NVIDIA driver 2026-07-12 22:32:52 -07:00
Hanzo AI ef128fadcc chore: drop watchdog script — hanzo gpu connect --studio-dir supervises the render backend (one way) 2026-07-12 22:25:03 -07:00
Hanzo AI 35f4a0328c merge: zen-video headless text-to-video queue (lint-clean) 2026-07-12 22:14:12 -07:00
Hanzo AI 9978f26e25 fix(zen-video): lint — split imports, JSON to stdout without print 2026-07-12 22:14:12 -07:00
Hanzo AI 5ed1dacbdc fix(dispatch): route BYO-GPU jobs before coordinator validation
A GPU-less cloud pod validates ckpt_name & friends against ITS OWN (empty)
model list, rejecting every checkpoint-referencing graph with 'not in []'
before dispatch could ship it to the org's GPU box. Validation belongs where
execution happens: the dispatch attempt now runs first, and the worker
validates against its real node classes + models. Orgs with no online GPU
fall through to local validation unchanged.
2026-07-12 22:08:44 -07:00
Hanzo AI 4200c52000 fix(packs): comfyui_segment_anything under Transformers 5; installer applies scripts/patches/<pack>-*.patch
BertModelWarper now delegates to the real BertModel.forward instead of
reimplementing internals whose private signatures drift between releases;
checkpoint weights remap bert.* -> bert.bert_model.* at load. The installer
force-checkouts each pin (discarding prior patches) then applies any
scripts/patches/<pack>-*.patch, so re-runs stay idempotent and the image
build gets the same fix the GB10 venv had by hand.
2026-07-12 22:08:44 -07:00
Hanzo AI 1d67adb0fb feat(scripts): karma library manifest generator + studio watchdog (from the GB10 box)
karma_manifest.py regenerates the ONE canonical library.json index of the
karma org's rendered assets (storefront + socials queue both read it).
studio_watchdog.sh keeps the local render backend on :8188 alive unattended;
paths derive from the script location (STUDIO/STUDIO_PY overridable).
2026-07-12 22:08:44 -07:00