Compare commits

...
Author SHA1 Message Date
hanzo-dev 91cbfd81d4 feat: per-GPU gpu-jobs queue + worker-mode gate (no hidden runs)
Make EVERY studio render flow through the org's visible gpu-jobs queue and
eliminate the direct-to-127.0.0.1:8188 bypass. Studio-side only (cloud CLI /
worker two-lane claim owned by the cloud agent).

- gpu_dispatch: per-GPU targeting. X-Target-GPU -> taskQueue "gpu:<identity>"
  (namespace stays gpu-jobs); an explicit target always enqueues. Untargeted =
  shared "gpu-jobs" lane.
- server.post_prompt: in --worker-mode refuse a direct /prompt POST without
  X-Worker-Token (403) via worker_client.reject_untrusted_worker_submit (one
  policy gate). The only accepted execution is a claimed job over
  /v1/worker/execute. Legacy prompt_router push is OFF unless
  STUDIO_LEGACY_PUSH_ROUTER=1 -> gpu-jobs enqueue is the ONE submit path.
- worker_client: /v1/worker/execute validates X-Worker-Token, runs the graph on
  the local prompt_queue, scopes outputs to the job's org, returns {prompt_id}.
- studio_home: /v1/render-queue + /v1/nodes are AUTHORITATIVE — read the cloud
  tasks engine's gpu-jobs activities (real claiming identity, status ->
  queued/running/done/failed, lastHeartbeatTime, per-node depth). render_jobs.json
  is only a ~2s pre-claim placeholder. X-Target-GPU forwarded through _queue_prompt.
- UI: "Run on <gpu>" chips (studio_home.html/shell.js) set X-Target-GPU; in-flight
  rows show the REAL worker + queued/running state.

Tests: gpu_dispatch_test (targeting), worker_seam_test (gate 403 + claim seam).
378 passed. Follow-up (cloud agent): content/studio_render.go:86 convergence.
2026-07-21 12:45:59 -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-devandz f5e7483570 style(studio): clear pre-existing ruff E702/F841 in 3D + video tests
`ruff check .` (the Python Linting gate) had been red on main since the
3D-lane commit: E702 semicolon-joined statements in
studio_home_model3d_test.py and an unused `root` local (F841) in
studio_home_video_test.py. Split the statements onto their own lines and
drop the dead assignment. No behavior change; both suites stay green.
2026-07-18 17:31:25 -07:00
hanzo-devandz d45067db4d fix(studio): anon /studio nav → 302 /login, not a raw 401
The IAM auth gate refused any unauthenticated request whose Accept lacked
`text/html` with a 401, so a plain browser/curl navigation to a protected
page (e.g. /studio) hit a dead 401 instead of the login flow.

Invert the rule and decomplect it: a top-level browser navigation is now
redirected (302) to the one server-owned login entry point,
`/login?next=<path>` — which starts the OIDC code flow and returns the user
where they were. Only a request that self-identifies as API/XHR/JSON
(Sec-Fetch-Mode != navigate, X-Requested-With, or Accept: application/json
without HTML) still gets 401, so the SPA's own fetch calls handle re-auth
instead of following a redirect into HTML. An ambiguous client (plain curl,
Accept: */*) counts as a navigation and is redirected — a human never sees a
raw 401. handle_login honors a sanitized ?next (open-redirect/loop-safe).

Bump 0.17.18.
2026-07-18 17:31:25 -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
studio-implandHanzo AI 3565974382 studio: video lane — generate/card/ingest/judge over the shared library and flywheel 2026-07-18 02:02:30 -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
baa40cf16c studio fix: rebuild the fix graph on the proven family + honesty guard + hygiene (#28)
The /v1/library/fix flow degraded images and compounded the damage every pass —
the source of the noise-stipple "bad skin" epidemic across the library. Three
layers, all decomplected.

1. GRAPH. _fix_graph (and _compose_graph) dropped the two
   FluxKontextMultiReferenceLatentMethod nodes — a Flux-Kontext
   `index_timestep_zero` reference method grafted onto Qwen-Image-Edit
   conditioning. It survived on in-distribution studio shots but on real photos
   with large flat regions (sky/water) it baked latent-grid mosaic + skin
   speckle, and each fix-of-a-fix amplified it. What remains is exactly the
   render line's proven-clean family: TextEncodeQwenImageEditPlus reference edit
   into an EMPTY latent at denoise 1.0, model names pinned to the exact files on
   the worker (qwen-image-edit-2511 / qwen2.5-vl-7b / qwen-image-vae). Proven
   e2e on spark: the same input+seed that produced severe mosaic renders clean.

2. HONESTY GUARD. A >=10-step render that lands within 5s of dispatch cannot
   have sampled (a graph that errored into history, stale-history reuse, a
   degenerate self-POST). _finalize_landed — the ONE place a landed render goes
   terminal, shared by the queue poller and the work-log view — marks it FAILED
   ("engine did not sample") instead of delivering it, and skips a matched
   output that predates the dispatch (a same-prefix sibling, not this job).

3. ITERATION HYGIENE. When the chosen fix base is itself corrupt (quality-gate
   flagged, or deleted), _clean_base walks the lineage to the nearest clean
   ancestor and fixes from THAT, and the dialog says so ("Fixing from the
   original — this version is corrupted"). One mechanism in the dispatch path,
   used by both the route and the chat create capability.

Tests: graphs name only worker-present models and carry no Flux-Kontext
reference method; the implausibly-fast guard flips status; _finalize_landed
does done/failed/skip-stale; the ancestor fallback picks the clean root.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 16:55:18 -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
9e4d098af2 studio: cloud pod never renders locally — enqueue every render to gpu-jobs (#27)
The org-scoped fleet read is org-blind: a BYO GPU registered under a sibling
org (karma's workers live under hanzo) is invisible to a karma user token, so
dispatch_if_worker saw "no GPU", fell through, and the GPU-less coordinator
self-POSTed its own ComfyUI — an instant "Prompt outputs failed validation"
(node "studio pod"). Composes for karma died on this every time.

Two halves, one mechanism:

1. A model-less cloud coordinator NEVER renders locally. dispatch_if_worker now
   enqueues to the SHARED gpu-jobs lane whenever this box has no models of its
   own, even when the org-scoped fleet read shows none — the job WAITS for a
   worker (workers drain the shared queue regardless of which org they register
   under). A box WITH its own models still renders locally when it sees no
   worker. A queued job that waits beats an instant fake failure.

2. The capacity gate (/v1/gpu-status → the ⚠ warning) is advisory ONLY and now
   reads the fleet with a SERVICE identity (STUDIO_WORKER_TOKEN, then
   STUDIO_COMMERCE_TOKEN), user token as fallback — so every org sees true
   worker availability. Job tenancy is untouched: dispatch still enqueues under
   the user token; only the org-blind gate changed.

Tests: karma-org model-less dispatch enqueues (never local, never on POST
failure); a box with models still renders local; online worker keeps its label;
the advisory gate uses the service identity first and falls back to the user
token.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 16:12:58 -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
4c77c82b90 Stacks: Procreate-style gallery folders (per-org SQLite) (#26)
Full Stacks spec on the /studio home surface: create by drag (image→image) or
multi-select; layered Stack cards (cover/name/count/updated); a detail view with
add/remove/reorder/move/set-cover/rename/describe/unstack/delete; the delete
dialog defaulting to preserve; generation 'Save outputs to' routing (fix/compose/
rerun/template) with per-org auto-stack-batch setting; keyboard + touch parity
(no drag-and-drop ever required); a11y labels + live region; loading/empty/missing
states.

Backend: middleware/stacks_store.py — per-org SQLite (WAL, BEGIN IMMEDIATE) with
the spec's stacks + stack_assets join, UNIQUE(asset_id) = one Stack per asset,
tenancy structural (one file per org). Replaces the JSON whole-set /v1/stacks
store with the full REST surface. Ingest assigns membership; remove != delete;
delete-images maps to the library soft-delete. Tests: tests-unit/studio_home_test/
test_stacks.py.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 15:43:32 -07:00
a6c1130b54 studio-chat: Up next / History / Stacks queue surface with ratings + favorites (#22)
* studio-chat: Up next / History / Stacks queue surface with ratings + favorites

The studio-chat SPA's right rail becomes the full YouTube-watch-page surface,
shaped around generative flows (a card = a run in a flow):

- Up next: the live queue (GET /v1/queue/status joined to /v1/worklog) — pinned
  "Next" card + vertical cards, thumb-left with an ETA/elapsed corner badge,
  2-line prompt, node · time · status · position meta, per-card overflow menu.
  Delete (POST /v1/queue/cancel) and Add references (file pick + drag-drop →
  /upload/image → POST /v1/queue/amend) on each queued card.
- History: past runs newest-first (GET /v1/worklog +?q= search), output
  thumbnail, "New" badge on just-landed renders, version pips for runs with
  descendants → a walker over GET /v1/worklog/lineage.
- Rate 0-5 stars + a note ("What should the AI do differently next time?")
  per run — the flywheel row {path, stars, note, ts} keyed by run id.
- Favorite (heart) → offer "turn this into a template" (name it → confirm what
  it captures → POST /v1/templates with a captured flow).
- Stacks: drag one card and HOLD (~600ms) over another to form an iOS-style
  folder; stacked-cards tile + count; click = folder-open reveal; unstack to
  dissolve. Persisted per org.
- Filter chips (All · Queued · Done · Favorites · Stacks), resizable main|rail
  and Up-next|History dividers, per-section collapse, active filter — all
  persisted in localStorage keyed by org+user; "Reset layout" in the rail menu.
- Mobile (<=820px): single-scroll column, rail as the bottom section, composer
  fixed to the viewport bottom with safe-area inset; example-prompt chips; a
  44px account avatar; AA-contrast grays; real send-button state pair.

Middleware (all tenant-scoped via _org_of, one atomic per-org store each):
- GET/POST /v1/favorites (favorites.json) — the ONE favorites store; the legacy
  studio_home.html favorite toggle migrated onto it too (was ratings.favorite).
- GET/POST /v1/stacks (stacks.json) — normalized, >=2 members, bounded.
- POST /v1/library/rate: carries `path` for the flywheel; favorite removed
  (now its own store — one way).
- POST /v1/templates: accepts a `flow` payload, not only a node graph.

Tests: tests-unit/studio_home_test extended (favorites, stacks, rate stars+
notes+path, flow templates) — tenancy proven on each; added to the hanzo.yml
unit gate. web build green.

* studio: self-heal stale-asset dispatch — refresh + "pick it again", never strand

When the item a user clicked was deduped/renamed/cleaned since their page loaded,
a fix/compose/rerun dispatch 400s "unknown asset: <path>" and the raw error left
them stuck. One self-heal wherever a dispatch happens:

- Legacy studio_home.html: healStale(j) — on an unknown-asset error, close any
  open dialog, drop the stale selection/tray, re-fetch the library (load()), and
  toast "That item changed — library refreshed, pick it again." Wired into the
  fix + compose (sendRef), compose (sendCompose), and rerun (bulkFork) branches.
- studio-chat SPA rail: heal(e) — a stale run (unknown asset / not found / no such
  job / already finished) on cancel or amend refreshes the queue and shows "That
  run changed — refreshed, pick it again." instead of the raw error.

* rail: bigger card thumbnails + a visible × on every card (cancel / soft-delete)

- Thumbnails grow to ~half the card width and a taller 4:5 crop (44% on mobile)
  so the render is actually legible in Up next and History.
- Every card gets an always-visible × (fast path) beside the ⋯ overflow menu —
  ONE shared handler, no second mechanism: a queued run cancels immediately
  (POST /v1/queue/cancel); a finished result is a destructive soft-delete
  (POST /v1/library/status status=deleted) and asks first with a small inline
  confirm. History hides soft-deleted results (optimistic + reconciled from the
  org's deleted set via /v1/library) so the delete persists across reloads.

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 15:28:42 -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
99919db60d gate: corrupted renders never surface as library items (#25)
Every render lands in the library through the one ingest choke point,
POST /v1/library/upload -> _index_upload. The moment that creates a NEW
asset row, a quality judgment fires out-of-band (aiohttp task, never
blocks the upload response): the just-stored image is downscaled to a
~512px JPEG and posted to the cloud vision judge (zen-vl) for a strict
PASS/FAIL on the observed corruption classes -- mottled/blotchy/speckled
skin, ghosting/double-exposure, frame-within-frame borders, glyph garbage.

A clear FAIL moves the asset draft -> flagged (a new first-class status
with deleted's visibility semantics: excluded from every default grid,
recoverable) and annotates its work-log row. Everything else -- PASS,
inconclusive, 402/429/timeout/unreachable, unreadable image -- leaves the
render a normal draft. The gate fails open: it can only ever hide clearly
broken work, never good work, and never blocks a render from landing. A
late verdict only ever transitions draft, so it can't yank curated work.

- middleware/quality_gate.py: the mechanism (judge prompt, strict verdict
  parse, downscale, fail-open run, fire-and-forget schedule). Same bearer
  the upload carried -> tenant-isolated cloud billing, one auth path.
- studio_home: _STATUSES gains flagged; _index_upload reports new rows;
  the upload route schedules the gate on a newly-surfaced render.
- worklog.add_event: annotate a completed row without touching its status.
- studio_home.html: flagged hidden from normal grids, a Flagged review
  filter, Restore -> draft.
- /v1/library/status accepts flagged (flag + restore). Sweeper preserves
  and tallies flagged like deleted (verified).

Config: STUDIO_GATE=off kills it; STUDIO_GATE_MODEL (default zen-vl);
STUDIO_GATE_URL override for local engines. No other knobs.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 15:13:30 -07:00
fac9880aaf library: hidden segments are never assets (#24)
The everything-indexes sweep pulled .thumbs/ cache files in as rows; fixing
one sent a thumb path into the dispatcher (unknown asset). One rule, both
writers: any path segment starting with a dot is excluded — forks and cache
trees alike.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 13:59:52 -07:00
bb3711eca1 manifest: taxonomy enriches, never gates (#23)
The sweeper rebuilt library.json and DROPPED every image classify() could
not parse — erasing ingest-indexed renders on each cycle (rows appeared,
then vanished minutes later). Unclassifiable images now keep a minimal row,
prior fields preserved.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 13:17:47 -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
3f48ca15e0 ingest: index on dedup too — files without rows must repair (#21)
An interrupted mirror leaves bytes on disk with no library or worklog row;
the dedup branch skipped indexing, so re-ingesting repaired nothing. The
library upserts by path; the worklog now upserts by output. Recovery is a
re-POST of the same bytes.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 12:24:19 -07:00
fdc2da4c57 upload: 100MB cap — 4k masters run 30-60MB and belong in the library (#20)
Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 12:21:56 -07:00
9993da806e chat: surface the server's error message — a 402 says where to add credits (#19)
Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 12:14:44 -07:00
b5bbbfd724 library: dotfiles are never assets (#18)
The indexer skips them; _safe_asset refuses them (one gate for file serving,
fix sources, and every other path lookup). AppleDouble forks carry image
extensions, serve resource-fork bytes, and rendered 0x0 across the library.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 12:14:02 -07:00
73abeb1109 chat: same-origin /v1/agent bridge — the cookie never leaves the host (#17)
The SPA called the gateway cross-origin; the session cookie is host-only, so
every signed-in user got 403 on their first message (401 once the cookie was
widened — the gateway takes bearers, not cookies). The bridge forwards the
caller's bearer server-side, the wallet pattern. VITE_AGENT_API still selects
direct mode.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 11:52:55 -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
c2f16445f2 upload: reject dotfiles and sniff magic bytes (#16)
* upload: reject dotfiles and sniff magic bytes — the name lies, the bytes never do

AppleDouble forks (._foo.png) passed the extension check; 700+ poisoned a
library within an hour of the mirror going live. PNG/JPEG/WebP signatures
required; hidden names rejected.

* upload: size cap precedes the sniff; test payloads carry the full PNG signature

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 01:18:17 -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
93688cac99 worklog: widen ?q= search to paths + refs, not just prompt (#15)
An ingested render row has an empty prompt, so filtering Queue & History by
filename found nothing. Widen the /v1/worklog ?q= filter to also match kind,
output_prefix, output (the landed path), and refs — so a render is findable by
its filename or source image, not only by a typed prompt. Test covers filename,
ref, and prompt matches.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-17 00:49:53 -07:00
be313e8068 Library ingest: POST /v1/library/upload — every render lands in the library (#13)
* Library ingest: POST /v1/library/upload — every render lands in the library

Tenant-scoped ingest so EVERY render is stored in studio.hanzo.ai, including
renders produced on a GPU node outside the job path (the node's mirror loop
POSTs here). Org via _org_of like every route.

Body: multipart (field image/file) OR raw bytes + ?name=, optional ?subpath=
(default renders). Writes orgs/<org>/output/<subpath>/<name> atomically
(tmp+replace, same pattern as the work log). Rejects traversal (basename-only
name + allowlisted subpath chars), caps size at 30MB, and skips a byte-identical
existing file (name+size match → 200 {ok, existed:true}); else 200 {ok, path}.
The manifest sidecar already indexes output/ — no other wiring.

Tests (studio_home_test): round-trip via GET /v1/library/file, dedup, tenant
isolation (org A upload never visible to org B, no cross-tenant overwrite),
subpath-traversal rejected, traversal-y name neutralized to basename, non-image
rejected, oversize rejected. 259 passed.

* Ship scripts/ whole (unbreak manifest sidecar) + index renders at ingest

(A) Regression: .dockerignore `scripts/*` + `!install_custom_nodes.sh` dropped
scripts/library_manifest.py from the image, so the build-manifest sidecar's
`[ -f ]` guard silently no-op'd and NOTHING got indexed into any org library.
Ship scripts/ whole again.

(B) Make indexing event-driven at the ingest point: POST /v1/library/upload, after
writing the asset, atomically appends the org library.json entry ({path, design?,
kind, role?, status:"draft", tags:[], updatedAt}) AND a worklog row ({kind:"render",
node, prompt:"", refs:[], parents:[], output_prefix:<path>, status:"done"}) — so
every mirrored render is instantly findable in Assets AND Queue & History with its
source node, filterable by run/flow, the moment it exists. Optional ?design= ?kind=
?role= ?node= with safe defaults (node→"upload"). Upserts by path (no dup on
re-render); the existed/dedup path skips indexing. The manifest sidecar stays the
sweeper for files that appear outside the route. _row_output resolves an ingested
render's output (its stored path) so it shows a thumbnail + opens its graph.

Tests: upload → library.json entry + worklog row appear (node, done, resolved
output), tenancy holds, dedup adds no second entry. 260 passed.

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 22:19:20 -07:00
6b5597d8cb home: account menu — the chip advertised one and did nothing (#14)
cursor:pointer + title=Account with no handler. Tap now opens a menu:
identity, Manage account (hanzo.id), Sign out (/logout — existing route).

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 21:52:03 -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
86bb0a3cad Shell: Studio/Editor toggle, sidebar chat, templates, wallet, GPUs (#11)
One shared shell asset (middleware/shell.{js,css}, served at /studio/shell.*)
is loaded by BOTH views — the Studio home <script>-includes it, and the Editor
gets a single tag injected into the engine index.html by server.get_root. It
renders one chrome in both: a Studio/Editor toggle, wallet chip, GPUs badge +
panel, and a dockable right chat sidebar backed by the ONE chat path
(/v1/copilot/chat). Same-origin cookies carry the IAM session, so login/org are
inherited — no auth is rebuilt.

New tenant-scoped routes (all via _org_of):
  GET  /studio/shell.js|css
  GET  /v1/workflow?id=<worklog id>   graph from the item's output PNG (404 if
                                      not owned / not landed) — Open in Editor
  POST /v1/templates                  save {name, graph} → orgs/<org>/templates
  GET  /v1/templates                  list
  GET  /v1/templates/{name}           fetch
  POST /v1/templates/render           queue through the ONE dispatch funnel
  GET  /v1/wallet                     display-only proxy of the user token to the
                                      cloud finance balance → {org, balance, currency}

Studio home: "Advanced mode" link replaced by the shell toggle; dead chatfab CSS
removed; Templates tab lists org templates (Open in Editor + Render); work-item
context gets Open in Editor; favorite→template now writes a REAL template.

Mobile (verified 390x844 + 768x1024): chat collapses to a full-width bottom
sheet; the pill bar stays one compact line (icon-only Chat/GPUs) so all controls
stay reachable and clear the content; zero horizontal overflow.

Wallet is display + Top up link (pay.hanzo.ai) only — no card fields, no minting.
Tests extend studio_home_test for the new routes (tenant isolation + shape).

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 20:44:50 -07:00
96a633d3ce render: cap startToClose at 4h — the tasks default (~1h) reaped live renders (#12)
Liveness is the 1200s heartbeat; the cap only bounds a live-but-stuck render.
Observed: activities reaped at ~68m mid-render, queue re-ran the same job six
times, results stranded on the node.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 19:31:45 -07:00
722ea7fd39 Queue transparency: which node runs it + position/ETA/elapsed (#10)
Makes the unified queue honest about WHERE and WHEN, all server-computed and
tenant-scoped (an org sees only its own jobs' positions and its own nodes).

WHERE — node identity:
- gpu_dispatch reads whatever the gpu-jobs fleet publishes (_node_label prefers
  name/hostname/label; home-lab labels spark/dbc/evo land there) — no parallel
  registry invented. dispatch_if_worker records the target node (the single online
  GPU is definitive; ambiguous 'gpu' with several) on the render_jobs row; _dispatch
  copies it onto the work-log row. Local-core jobs are 'studio pod'.
- GET /v1/nodes: the org's GPU nodes (online/offline) + the pod, for header badges.
  Fleet is token-scoped → only the caller's nodes.

WHEN — position + ETA (server-side, never client math over private data):
- worklog records queued/started/finished timestamps (mark_started when a job first
  heads its lane; mark_finished from the output file mtime). Pure functions:
  median_durations (rolling median render seconds per kind) and estimate_lanes
  (position N of M per lane; wait ≈ jobs-ahead × medians + running remaining;
  eta = wait + own median). Unknown kinds fall back to a default; labeled honestly.
- GET /v1/queue/status: this org's active jobs with {position, of, node, status,
  elapsed_seconds, wait_seconds, remaining_seconds, eta_seconds} + medians. Reads
  the org work log only; no fleet call on the hot path.

UI: 'Your work' rows and the BYO-GPU row show ⚙ node · #N/M · running <elapsed> ·
~Xm left  /  #N/M in line · ~X min est. Queue header shows per-node online/offline
badges + the median-render stat. Auto-refresh while the queue view is open (6s
positions, 20s nodes — no hammering); the running item's elapsed ticks each second
client-side; a completion toast fires when an open page sees a job land.

Tests (11 new): _node_label + online-node filtering; record-node; started/finished
+ median; median_durations per kind + ts fallback; estimate_lanes positions/ETA +
default-median. Route smoke: positions #1/2/3 with ETAs from the org's own median,
landed→finished drop, globex sees none of acme's queue, /v1/nodes maps fleet→badges.
Browser smoke: node+position+ETA render, elapsed ticks 0s→2s, median stat + pod
badge, zero console errors.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 16:28:30 -07:00
845ddd9dae Cancel + amend queued jobs (tenant-scoped, both realms) (#9)
Cancel and amend any of the caller's OWN queued/running renders, resolved
server-side; a prompt_id that isn't the caller's 404s (never reveals it exists).

Cancel — POST /v1/queue/cancel {prompt_id}:
- local pod queue: delete a pending item OR interrupt a running one, org-scoped
  (reuses the core prompt_queue primitives — one tenant can't touch another's).
- BYO-GPU lane: drop the render_jobs.json tracking row (per-org file → inherently
  owned); a job already rendering on the worker may still finish (the pod can't
  stop a remote process) — the UI says so honestly.
- marks the work-log row cancelled. _owns_job gates it: work-log OR org-stamped
  local-queue item OR the org's gpu tracking file; else 404.

Amend — POST /v1/queue/amend {prompt_id, instruction, paths?, uploads?}:
- honest atomic swap (queued graphs are immutable): cancel the original, then
  re-dispatch through the ONE funnel with the amended prompt + the original refs
  (kept, read from the authoritative work-log row) + new library picks/uploads.
  Records a fresh work-log row; position resets, shown honestly in the UI.

UI: Edit + Cancel on every queued/running work row and in the context panel;
Cancel on the BYO-GPU live row; the local 'Remove' now routes through the unified
cancel (dead delQueued removed). Amend reuses the Fix dialog in amend mode
(openAmend): prompt primed, current refs shown locked, add more via the same
dropzone + history picker. Errors surface in-dialog (no silent swallow).

Hardening: _queue_prompt tolerates a non-JSON downstream response (reports it
instead of 500-ing).

Tests: cancel org-scoped + gpu-row removal + cross-tenant not-owned; amend atomic
swap keeps original refs + adds new, unknown→NotOwned(404), terminal rejected (5).
Route smoke: amend swaps + re-queues, gpu cancel removes tracking, globex gets 404
on acme's job (cancel AND amend). Browser: Edit/Cancel on queued rows, amend dialog
primed with prompt + current ref, correct POST, zero console errors.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 13:24:24 -07:00
f569319988 Work history with full context + parent→child lineage/versions (#8)
Adds a per-org work log — the ONE record of every render dispatched — as the
backbone for a queue/history that never loses a job and shows what produced it.

Backend (middleware/worklog.py, one JSON per org at orgs/<org>/output/worklog.json;
same pattern as library.json/render_jobs.json; no new DB, no backfill):
- The single dispatch funnel (dispatch_fix/dispatch_compose/rerun) records one row
  at dispatch via _dispatch(): {id, ts, kind, prompt, refs, uploads, parents,
  output_prefix, params, lane, status, events}. A FAILED dispatch is recorded too
  (status=failed + reason) so the deployed 'nothing showed as queued' can't recur —
  every dispatch is visible with real status, gpu-lane or local.
- GET /v1/worklog — org's dispatched renders, reverse-chron, ?q= over prompt+kind,
  status enriched (done when the output lands). Server-resolved org only.
- GET /v1/worklog/lineage?path= — a library asset's version chain (ancestors it was
  derived from + later fixes), a pure function over this org's rows.
- _landed_output matches SaveImage's exact <prefix>_<counter>_.png (not a loose
  <prefix>*, which collapsed distinct lineage steps).

Frontend (studio_home.html):
- Queue & History → 'Your work': searchable, reverse-chron list with status badge +
  lane + prompt + thumbnail; click → context panel (prompt, reference/result thumbs,
  workflow family, params, process trail, and the Versions chain inline).
- 'Versions' affordance on every library card → the same lineage chain; click any
  version to open it.
- 'Reuse context' / 'Fix this version' open the Fix dialog primed with the item's
  prompt + references (openFixPath/primeFix) — reuse = continue from any version.

Tenant isolation: one log file per org; routes resolve org server-side; lineage is
pure over the caller's rows — cross-tenant leakage is impossible by construction.

Tests: worklog record/failed/status/cap, lineage chain + cycle-termination,
cross-tenant isolation (7). Route smoke: dispatch→done+output, 2-step lineage,
failed dispatch recorded with reason, globex sees none of acme's work. Browser
smoke: work list + status badge + context panel + Versions chain, zero console
errors.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 13:15:21 -07:00
c249ac41a4 Fix live blockers: surface dispatch errors + working '+' attach (no silent swallow) (#7)
Two user-reported live regressions on studio.hanzo.ai, root-caused at the
deployed image (sha-0d307dc, pre-#6):

1) Fix 'won't work / nothing queued' — the GPU-less prod pod fails /prompt
   validation (models absent) or the router has no GPU worker; _queue_prompt
   raised web.HTTPBadGateway(text=json.dumps(body)[:500]) — a raw 502 whose
   TRUNCATED body the dialog couldn't parse, so the error was swallowed into an
   empty 'Failed:'. Now _queue_prompt raises DispatchError carrying the REAL
   reason (_prompt_error_message), which the fix/compose routes already return as
   {"error"}. Every dialog/sender now SHOWS backend errors persistently and keeps
   the dialog open on failure — no silent swallow anywhere on fix/compose paths.

2) The composer '+' attach button did nothing (no handler). Wired it to the
   shared upload lane (POST /upload/image → org input dir, multi-tenant) with a
   visible thumbnail; added drag-and-drop + paste to the Fix dialog AND the
   composer, all through ONE shared helper (uploadImage/uploadMany/wireDrop/
   wirePaste). Upload failures are shown, never swallowed. Reference tray now
   holds library AND uploaded refs; a single uploaded photo drives a fix via the
   new dispatch_fix(upload=) base (edit a fresh photo without landing it first).

Tests: text-only fix regression lock, uploaded-base fix + traversal refusal,
_prompt_error_message extraction, and a real-aiohttp test that a non-200 /prompt
surfaces the real reason (not a 502). Browser smoke (headless Chromium on the
served page): '+' opens the chooser and uploads, drag-drop adds a ref, text-only
Fix posts {path,instruction}, zero console errors.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 13:00:40 -07:00
268857c16e Fix surface: reference images via drag-drop upload + add-from-history (#6)
The Fix action on a generated image becomes a reference-capable surface:
- base image (the clicked output) + up to 4 references
- drag-and-drop / click-to-upload image files as references, via the
  existing core /upload/image route (org-scoped input dir — the same place
  fix/compose already stage refs and the BYO-GPU collector ships from)
- add-from-history picker: attach past library generations as references
- no references -> single-image /v1/library/fix (unchanged); any reference
  -> multi-input /v1/library/compose, base image first

Backend: dispatch_compose now accepts uploaded input-dir names (uploads[])
alongside library asset paths, validated as safe existing images; the 2-5
bound is on the combined reference count. One compose path, additive.

Tests: _resolve_uploads filtering, compose upload path + single-ref
rejection; corrected stale _STATUSES vocab (includes 'deleted').

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-16 12:36:47 -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-dev 0a3146e00f docs: de-tenant the manifest + home docstrings
Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 17:06:03 -07:00
hanzo-dev 8e55ecac9a refactor: karma_manifest.py -> library_manifest.py — org-generic name for the org-generic tool (unified multi-tenant SaaS; no tenant names in code)
Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 17:05:44 -07:00
hanzo-dev b6317999dd fix: dedupe version key from rebase resolution
Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 16:52:46 -07:00
hanzo-dev 5e9f72bc16 feat(studio): Studio Home — content-forward default view; node editor becomes Advanced mode
/studio: org-scoped asset gallery over library.json (status chips draft/
approved/queued/published), per-asset workflow provenance from the embedded
PNG graph, Re-run (exact graph, fresh seed) and chat-fix (instruction ->
Qwen-edit pass on that output) both through the ONE /prompt path with the
caller's own credentials. Pipelines tab reuses the 0.15.1 deep-link opener
for Edit/Run; STUDIO_HOME_DEFAULT=1 makes / land on /studio (SPA at
/?advanced=1). 5 unit tests.

Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 16:52:24 -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
f8007e6ced feat(zen-video): headless text-to-video queue on the GB10 (Wan2.2 via Studio)
Serialized, SQLite-persisted queue fronting Hanzo Studio (ComfyUI :8188) that
turns a prompt into a real MP4 on the GB10. One GPU => one job at a time; a
single async worker drains an ordered queue and submits to Studio.

Two composable surfaces over one backend:
- OpenAI-style /v1/videos/generations (gateway-injected X-Org-Id identity, per
  generated-second billing into a usage table).
- do-ai-compatible async /v1/videos + /{id} + /{id}/content (Bearer provider
  key) — the exact Sora-style create->poll->download contract the cloud LLM
  gateway's video client drives, so the ai registry adopts spark by pointing
  the zen3-video provider at spark, no ai-side client change.

Zen video family public names (zen3-video/-fast/-pro); private Wan2.2 filenames
confined to zen_wan_workflow.py, never returned to callers. Includes the
systemd unit and studio_clean_restart.sh (breaks the Studio JIT/EADDRINUSE
crash-loop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:11:05 -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
hanzo-devandHanzo AI 50c338ecfd feat(content): record completed renders as Assets in the content lane
A finished render becomes an `Asset` document — the DocType hanzoai/cloud
clients/content already declares (framework module "marketing") and already writes
itself when IT drives a render. Studio-initiated renders were the only ones with
no path into that record; this closes exactly that gap and adds NO second schema,
NO second lifecycle, and NO second storage plane. cloud is untouched.

  POST /v1/framework/Asset   (the already-live generic framework surface)

Same field shape clients/content writes (studio_render.go draftAsset): title,
kind, design, prompt, file, generator, workflow, source_prompt_id, render_params,
project, tags. `file` is the org-scoped output key (orgs/<org>/output/...) —
byte-identical to the key persistOrLink produces and the key Studio's own output
mirror lands, so the bytes are ALREADY persisted: this module uploads nothing and
holds no storage credential.

Written AS THE REQUESTING USER: the verified IAM access token rides to the prompt
worker on the queue's existing `sensitive` channel (never persisted into history)
and authenticates every write, so the Asset lands in that user's org with that
user's rights. No service account, no impersonation, no cross-org path.

Assets land in draft (the DocType default); the content lane's ONE state machine
(draft->in_review->approved->queued->published) owns every transition, and a human
does the promoting. Publishing is what a storefront shows, so an unattended render
can never push an image to karma.style.

Fire-and-forget on the same prompt-worker seam as billing: a render is never failed
because the lane was unreachable. Env-gated (STUDIO_CONTENT_PUBLISH); local dev
unaffected.
2026-07-12 22:05:54 -07:00
hanzo-dev 203375e5db ci: run linux jobs on hanzo-build-linux-amd64 ARC scale set (no GitHub-hosted builders)
Claude-Session: https://claude.ai/code/session_01RFrWpXc1BsqfrFYMbyDusJ
2026-07-11 00:22:48 -07:00
Hanzo Dev cea869bd14 feat(hanzo_engine): music + dub nodes, textured 3D, flagship pipeline
Add HanzoMusic (POST /v1/audio/music -> ACE-Step stereo AUDIO) and HanzoDub
(POST /v1/animate -> MuseTalk lip-sync: portrait IMAGE + driving AUDIO -> frames
+ audio). Fix HanzoImageTo3D to send the now-live `texture` flag (textured GLB
via SLAT + FlexiCubes) and offer obj. Correct HanzoTTS response_format to the
engine's actual wav/pcm. Drop the torchaudio dependency: studio_audio_to_wav_bytes
now uses the stdlib wave module, and studio_audio_to_data_url feeds the dub's
driving audio.

Ship user/default/workflows/hanzo-full-generative-pipeline.json: a prompt is
expanded by Chat and fans out to Image Gen -> (textured Image-to-3D AND WAN
Text-to-Video), plus TTS narration and ACE-Step Music -- every modality in one
graph, all on the native engine.

contract_check.py drives all 8 nodes over real HTTP against a stub engine that
enforces each Rust handler's request fields and the async job submit->poll->content
protocol.
2026-07-10 16:48:53 -07:00
Hanzo Dev ace9d057f8 feat(hanzo_engine): native-engine node pack over one shared client
Run Hanzo Studio AI workloads on the native-Rust Hanzo Engine instead of
in-process Python. Transport, config, and Studio<->engine type conversions
live in one client (client.py); each node is one endpoint, so a new engine
endpoint is a new small node with no framework change.

Nodes (category Hanzo/Engine): HanzoChat (SSE streaming, model dropdown from
/v1/models), HanzoImageGen (engine width/height/n schema), HanzoTTS
(/v1/audio/speech -> AUDIO), HanzoASR (/v1/audio/transcriptions -> text),
HanzoEngineRequest (generic /v1 escape hatch), HanzoVisionCaption,
HanzoTextToVideo, HanzoImageTo3D, HanzoSaveText.

Config: one way, HANZO_ENGINE_URL (default http://127.0.0.1:1234), /v1/* only.
Ships example user/default/workflows/hanzo-native-pipeline.json (LLM prompt ->
image prompt -> image gen -> save), e2e-verified against a CPU Qwen3-0.6B engine.
2026-07-09 21:16:03 -07:00
hanzo-dev 4d98076483 byo-gpu: ship uploaded inputs with the render job
Uploaded images land in orgs/{org}/input on this GPU-less pod; the BYO
worker renders on its own disk and cannot read them, so LoadImage of an
uploaded photo failed. Collect every LoadImage-referenced file that
exists under the org input dir, base64-inline it into the studio.render
job input (capped, traversal-safe). The worker materializes them before
rendering (cloud cli/gpu.go). Files already on the worker are harmless to
resend.
2026-07-08 15:50:46 -07:00
hanzo-dev 7ff75c0cce fix(dispatch): worker-mode nodes queue locally (never re-dispatch); absolute-import fallback for top-level server.py 2026-07-08 15:23:47 -07:00
hanzo-dev 28ed5bd9d1 feat(dispatch): tell the BYO-GPU worker where to return outputs (uploadUrl)
The studio.render job input now carries this deployment's public base (STUDIO_PUBLIC_URL,
default studio.hanzo.ai) as uploadUrl, so the worker POSTs finished renders back to
/upload/output here — org derived from the user's token, landing in orgs/{org}/output.
2026-07-07 20:39:47 -07:00
hanzo-dev fd02d0f6b9 feat(studio): token-authorized /upload/output for BYO-GPU gallery ingest
A remote hanzod gpu-fleet worker renders a studio.render job on its own GPU
and POSTs each finished image to /upload/output with the user's IAM bearer.
The IAM middleware validates the token and derives org from owner/groups, so
the file lands in this org's orgs/{org}/output (PVC + S3 mirror -> gallery).
No S3/rclone credentials on the worker box; the session token is the only
credential. Replaces the old rclone/S3-creds output sync.

- verify_jwt accepts a set of first-party audiences (STUDIO_IAM_ALLOWED_AUDIENCES)
  so a console/desktop/CLI-minted user token authorizes an org-scoped upload
  without a per-service re-mint (org still comes from owner/groups, not aud).
- image_upload forwards the request so /upload/image|mask|output are org-scoped.
2026-07-07 17:35:05 -07:00
hanzo-dev b24a2a12c1 fix(dispatch): accept browser session cookie (hanzo_token/access_token) as IAM bearer — mirrors iam_auth_middleware 2026-07-07 16:48:08 -07:00
z 49d300aba3 feat(studio): complete modality coverage — Kolors (t2i photorealism), Mochi-1 + Pyramid-Flow (permissive video), DiffRhythm (long-form music), CosyVoice2 (streaming/emotional TTS). All Apache-clean (PyramidFlow weights noted). Studio now spans every modality. 2026-07-07 14:21:22 -07:00
z 929671031b feat(studio): fill modality gaps — FOLEY (MMAudio video->audio, AudioLDM2 text->SFX, weights NC noted), audio source-separation (Demucs MIT), VibeVoice TTS (MIT). Verified real weight licenses (AudioLDM2/MMAudio weights are NC despite Apache code). 2026-07-07 14:16:38 -07:00
hanzo-dev c443d56f5c feat(studio): dispatch Run to org's connected GPU node via gpu-jobs (forwards user IAM token; org+project+billing derived cloud-side); in-pod fallback 2026-07-07 10:46:21 -07:00
z 43b7c60325 feat(studio): SOTA OSS across ALL modalities — video (WAN 2.2/LTX/CogVideoX/Hunyuan), voice/TTS (Kokoro/Chatterbox/F5), GGUF loader. Code-only, weights runtime BYO-license; non-commercial (F5 CC-BY-NC) + restricted (Hunyuan Tencent) weights noted. Fully OSS + customizable. 2026-07-06 11:26:42 -07:00
z d774d3f508 feat(studio): add music/song generation — ACE-Step (Apache-2.0, commercial-clean text->music w/ vocals+accompaniment). Code+weights permissive; LeVo (best-quality Tencent) BYO-license path noted. 2026-07-06 10:55:08 -07:00
z 40c26e3080 feat(studio): add 3D-gen + texture node packs — TRELLIS.2, Pixal3D, ComfyUI-3D-Pack (TripoSR/InstantMesh/gaussian-splat/texture-bake), Hunyuan3D wrapper. Code-only (permissive MIT/Apache); weights NOT shipped (runtime download, BYO-license); Hunyuan weights-license noted inline. No gate. 2026-07-06 10:30:12 -07:00
hanzo-dev 1063ee3d0e fix(sd1_tokenizer): restore canonical CLIP-L vocab (unbreaks FLUX.1 DualCLIPLoader)
The bundled studio/sd1_tokenizer/vocab.json had the token 'comfy</w>' scrubbed
(49407 tokens vs canonical 49408) while merges.txt still contained the 'com fy'
merge, so CLIPTokenizer.from_pretrained raised 'Error while initializing BPE:
Token comfy</w> out of vocabulary' for ANY FLUX.1 CLIP load (DualCLIPLoader type
'flux'). Only ever hit now because FLUX.1-Fill is the first CLIP-L consumer here
(Qwen/Klein use other tokenizers). Restored the canonical openai/clip-vit-large-
patch14 vocab.json (49408 tokens). Required by couture_best.json.
2026-07-05 18:34:07 -07:00
hanzo-dev 0337677c77 studio: add couture_best garment-swap workflow (2026 SOTA bake-off winner)
Head-to-head of 10 garment-swap methods on one fixed test case (pool_hires +
red_realistic). Winner = CatVTON-Flux native in-context transfer: SAM mask ->
InpaintCrop -> [garment|person] 768x1024 canvas -> FLUX.1-Fill + CatVTON LoRA ->
crop person half -> InpaintStitch (background byte-identical). Reproduces the
reference garment (true red, square neck, corset seams, ties) where Qwen-Edit
mottles the fabric and whole-image img2img smears the background.

- user/default/workflows/fashion/couture_best.json  parameterized (scene + garment -> Run)
- custom_nodes/hanzo-packs.txt  pin ComfyUI-Inpaint-CropAndStitch (GPL-3.0); note CatvtonFluxWrapper (bakeoff-only)

Model licenses flagged NC (FLUX.1-Fill-dev, Redux, CatVTON LoRA); Qwen-Image-Edit
(Apache-2.0) is the commercial fallback path.
2026-07-05 18:33:26 -07:00
hanzo-dev 0e774c87b6 studio: style login footer links (were default-blue underlined)
The bottom Docs/GitHub/Console/Discord row had no .product a rule, so it fell
back to the browser default link color + underline. Match the card's muted
console tokens (#cdd2da, no underline, hover -> #eef0f3).
2026-07-05 12:33:38 -07:00
hanzo-dev 201cfaf2f3 studio: console-match login + identity chrome, force Nodes 2.0 on
Task 2 — login/header look & feel to match console.hanzo.ai. The console is a
deliberately monochrome true-black system (no brand-purple; white primary
action; 9px radii; hairline #23262b borders), so the prior black+#8b5cf6 purple
login + identity pill did not read as the same product family. Restyled to
console's tokens:
- web/login.html: rebuilt as a console-style centered card (surface #050505,
  1px #23262b, 9px radius) with the 5-path Hanzo H mark, 'Sign in with Hanzo'
  as a white (#fbfcfd) primary button with near-black text — not the default
  ComfyUI splash.
- branding/hanzo-identity.css: pill + dropdown restyled monochrome to match
  console's OrgSwitcher/user menu (surface/border/text tokens, neutral avatar,
  red only for logout). Bumped identity bundle v2 -> v3 (cache-bust).

Task 3 — Nodes 2.0 (Comfy.VueNodes.Enabled) shipped as the only node renderer.
New idempotent branding/patch_vuenodes.py (wired as apply-branding step 12):
forces the registered default !1 -> !0, sets type boolean -> hidden so the
Settings dialog row is gone (ComfyUI-native, like Bookmarks.V2), flips the
inline ??!1 read fallbacks to ??!0, and deletes the logo-menu nodes-2.0-toggle
item. Same hidden treatment for the sibling AutoScaleLayout so the whole
'Nodes 2.0' settings category collapses. Verified: forced on, no user toggle.
2026-07-05 12:30:57 -07:00
hanzo-dev b289513c9d docs(LLM): custom-node packs mechanism + studio_compat shim 2026-07-04 13:20:01 -07:00
hanzo-dev e56eee9dc6 studio: identity menu shows Sign in only on 401, not local auth-off
Locally (IAM off) /v1/session returns 200 with authenticated:false; the
menu now renders the functional Local User pill instead of a Sign in
button whose /login 404s locally. The real signed-out state is the cloud
401 (auth on, no token) -> Sign in. Verified unmocked against the live
backend. Bundle v2.
2026-07-04 13:19:45 -07:00
hanzo-dev a9165125b5 studio packs: vendor top ComfyUI node packs as first-class hanzo packages
Add a comfy->studio import shim so upstream ComfyUI custom nodes run
unmodified against this fork's renamed core, then vendor the popular packs
(Impact-Pack/Subpack, IPAdapter_plus, controlnet_aux, Ultimate SD Upscale,
Frame-Interpolation, VideoHelperSuite, rgthree, Custom-Scripts, KJNodes,
cg-use-everywhere, SAM2, segment_anything) pinned to immutable commits and
reproducibly rebuilt in the image.

- studio_compat: one meta-path finder aliasing comfy*->studio* and mirroring
  Comfy*->Studio* API classes (the deferred phase-4 of scripts/rename_modules.py,
  done with the 3.12 find_spec API); activated by one import at top of main.py.
- nodes.py: honor the upstream `comfy_entrypoint` V3 extension name too.
- custom_nodes/hanzo-packs.txt: pinned pack manifest (SHAs + SPDX licenses).
- custom_nodes-requirements.txt + scripts/install_custom_nodes.sh: pack deps
  with the cu130 torch / NumPy 2.x / Transformers 5.x stack locked (derived
  from the live env, never downgraded); ultralytics installed --no-deps.
- Dockerfile/.dockerignore/.gitignore: image vendors the packs at build.

Live GB10 worker verified: node types 644 -> 1290 (+646), 0 import failures.
was-node-suite quarantined (hard numba dep incompatible with NumPy 2.5).
2026-07-04 13:19:09 -07:00
hanzo-dev a9d77be4e7 studio: IAM identity menu + session API + de-Comfy display strings
Top-right user menu driven by GET /v1/session (user, org switcher,
project scope, Console link, Logout). Org switching is real via one
hook: the auth middleware applies session.resolve_org to a COPY of the
validated user so iam_user["org_id"] -- the id every tenancy/output
path already reads -- reflects the selection; membership from
_orgs_from_claims (owner + organizations/orgs/groups). POST
/v1/session/{org,project} set validated cookies; /logout clears the
session + selection cookies and bounces to the IAM end-session -> /login.
Frontend is a SPA-agnostic pill + dropdown (branding/hanzo-identity.*),
injected like the copilot (patch_identity.py, apply-branding step 10).

patch_destring.py (step 11) sweeps English display 'ComfyUI' -> 'Hanzo
Studio' (boundary-safe: skips code identifiers, i18n keys, hyphenated
product names, and non-English locale bundles) and rebrands the Settings
section header value 'Comfy' -> 'Studio' / 'Comfy-Desktop' -> 'Studio
Desktop' while preserving the setting KEYS. Served English display
'ComfyUI' 63->0. Tests: middleware_test/session_test.py (12 new).
2026-07-04 13:15:51 -07:00
hanzo-dev b2e96ef256 copilot: chat sidebar that reads + mutates the workflow graph
Backend POST /v1/copilot/chat (middleware/copilot.py, registered in server.py)
runs one OpenAI tool-calling round to a chat endpoint (STUDIO_COPILOT_URL,
default local engine :1234 else api.hanzo.ai) and returns {reply, ops}. add_node
ops validated against nodes.NODE_CLASS_MAPPINGS so the model can't invent nodes.

Frontend bundle (branding/hanzo-copilot.js + on-brand css) registers a Copilot
sidebar tab via app.extensionManager.registerSidebarTab and applies ops against
app.graph (LiteGraph): add/set_widget/connect/set_prompt/move/delete/layout/queue.
Every op guarded. Injected by branding/patch_copilot.py (step 9 of apply-branding.sh),
survives frontend upgrades. window.HanzoCopilot.applyOps exposed for tests.

Tests: middleware_test/copilot_test.py (13, op-validation + mocked tool loop),
branding/copilot_smoke.py (headless: applier mutates a mock graph + panel renders).
Live route needs one server restart to register.
2026-07-04 12:19:39 -07:00
hanzo-dev 55d5c7a58e product workflows: worn-3D invisible-figure volume (reject flat-lay) 2026-07-04 11:27:12 -07:00
hanzo-dev fef673ade4 workflows: clean left-to-right layout (inputs -> prep -> conditioning -> sampler -> output) 2026-07-04 00:22:03 -07:00
hanzo-dev c0bd700407 studio: meter one render-completion event to Commerce (per-org, idempotent)
On a successful prompt execution, emit exactly one fire-and-forget usage
event to Commerce meter-events (POST /v1/billing/meter-events): userId =
IAM org_id, value = artifacts produced, idempotency = prompt_id. Timeout +
retry-once; a billing failure only logs and is dropped — a render is never
blocked or failed for billing.

Replaces the execution-time-priced record_usage (which billed against the
STUDIO_ORG_ID env, not the real IAM tenant) with metered record_render:
Studio reports raw quantity, pricing lives on the meter in Commerce.

Env-gated on STUDIO_BILLING_URL + STUDIO_COMMERCE_TOKEN + STUDIO_BILLING_METER
(all absent locally = no-op, zero behavior change). check_balance left as-is.

Tests: tests-unit/middleware_test/billing_test.py — success shape, idempotency
key, timeout-doesn't-block + retry-once, 4xx-no-retry, 5xx-retry, unconfigured
and partial-config no-ops. Full middleware_test suite green (52 passed).
2026-07-04 00:19:08 -07:00
hanzo-dev 8e6423dc30 theme: dark selected-state for sidebar icons (interface-panel-selected-surface was white) + hover/active state overrides 2026-07-04 00:13:10 -07:00
hanzo-dev e46945427d ci: bootstrap uv on runners that lack it 2026-07-03 22:19:36 -07:00
57bb837d35 fix(auth): route studio login through the server OIDC /callback flow (#5)
The SPA "Sign in with Hanzo" button hand-rolled an authorize URL
(IAM /login?redirect_uri=<studio root>), which the IAM default login app
(hanzo-console) rejected because the studio root is not in its allowed
redirect-URI list -- blocking all customer login.

Give the flow exactly one entry point: the button now links to the studio
server's own /login route, which runs the middleware's OIDC Authorization
Code flow (client_id=hanzo-studio, redirect_uri=<origin>/callback, PKCE +
signed state). /callback is already an allowed redirect URI, so login
completes and lands on /. Removes the client-side URL builder and the dead
{{IAM_URL}} templating.

Tests: /login is public; middleware exposes handle_login; unauth /login
builds the hanzo-studio authorize URL to /callback; post-login return path is /.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-03 21:38:48 -07:00
hanzo-dev 99c3d53d8b studio: seed 25-tab fashion workspace idempotently (guard v3)
Move the workspace-tab seeder out of a hand-edited index.html and into a
dedicated branding patcher wired into apply-branding.sh, so it survives a
frontend-package reinstall and re-applies on every branding pass.

Root cause the seeder addresses: the frontend restores open tabs from
localStorage via getStorageValue/setStorageValue, whose clientId-suffixed
sessionStorage layer is read BEFORE plain localStorage. A previously-saved
1-tab shadow (Comfy.OpenWorkflowsPaths:<clientId>) masked the seed, and the
old guard was burned at v2 so existing profiles never re-seeded.

Fix: bump guard v2->v3 (existing profiles re-seed once) and purge the stale
sessionStorage shadows on re-seed so the fresh localStorage seed wins even on
a same-tab reload. Clean-profile boot now opens all 25 fashion workflows
(alongside ComfyUI's own default tab).
2026-07-03 20:49:21 -07:00
hanzo-dev 4ab38a1f06 studio: crash-durable SQLite render queue + engine selector
SqlitePromptQueue (middleware/tasks_queue.py): drop-in for PromptQueue backed
by one SQLite file (stdlib sqlite3, WAL, zero external processes). Opt in with
STUDIO_QUEUE_DB; precedence STUDIO_QUEUE_DB > STUDIO_PERSIST_QUEUE > memory so
local default is unchanged. The put/get/task_done seam maps to a durable job
state machine: idempotent submit (INSERT OR IGNORE on prompt_id), exactly-once
claim (BEGIN IMMEDIATE), retry-on-error, and crash-recovery via a lease + reap
that re-runs an abandoned claim (idempotent — SaveImage suffixes increment).
Multiple Studio processes on the same DB compete safely.

Engine selector (middleware/engine_selector.py): GET /v1/engines lists execution
targets for the org (local + registered compute_config workers), PUT
/v1/engines/default sets a per-org default stored on ComputeProfile; route_prompt
honors it (local = unchanged). Leased cloud machines are a future engine class
(interface stubbed). Frontend picker is a TODO.

docs/federation.md rewritten to the durable-queue reality with one future
paragraph (remote Tasks backend + Go/unified-binary HIP-0106 migration + leased
machines). Tests: middleware_test/{tasks_queue,engine_selector}_test.py, incl.
crash-recovery across queue instances.
2026-07-03 15:43:42 -07:00
hanzo-dev cea6d8029e ci: target arc scale set hanzo-build-linux-amd64 by name
ci@v1 defaults runs-on to [self-hosted,linux,amd64]; ARC scale sets are
matched by installation name, so the job sat queued with no runner. Pin the
runner to the hanzo build pool (same as cloud/release.yml).
2026-07-03 15:27:03 -07:00
hanzo-dev e632401bc5 ci: drop legacy deploy.yml (superseded by cicd.yml); make ruff green
- Remove .github/workflows/deploy.yml — the canonical build+test+deploy path
  is cicd.yml (imports hanzoai/ci build.yml@v1, reads hanzo.yml). One way to
  build. The old Docker workflow called hanzoai/.github docker-build.yml and
  failed at 0s.
- prompt_queue_persist_test.py: drop the 4 diagnostic prints (T201); pytest
  reports pass/fail and the standalone runner raises on failure.
- zen_video_adapter.py: remove unused json/asyncio imports (F401).
2026-07-03 15:13:51 -07:00
hanzo-dev b521933ca3 fix(server): graceful SIGTERM/SIGINT shutdown + crash-durable queue
Root cause of the "Event loop stopped before Future completed" crash that
wiped the in-memory queue/history (seen 3x today): the fork's _graceful_shutdown
handler (added in a45a441) called event_loop.stop(). The server coroutine passed
to run_until_complete() never completes on its own, so stopping the loop makes
run_until_complete() raise RuntimeError; the surrounding try only caught
KeyboardInterrupt, so the process died non-zero with a traceback and discarded
the queue. The dirty exit also failed to release :8188 promptly, and a duplicate
transient/user `hanzo-studio.service` racing the canonical studio.service turned
that into an EADDRINUSE restart storm.

main.py:
- handler is now idempotent, flags the queue closed, snapshots it, then stops
  the loop; the outer try treats RuntimeError('Event loop stopped before Future
  completed.') as the expected graceful-exit path (logs "Server stopped by
  shutdown signal", exits 0). Verified live in journald.

execution.py:
- opt-in crash-durable PromptQueue behind STUDIO_PERSIST_QUEUE=1. Pending +
  in-flight prompts are snapshotted atomically to user/queue_snapshot.json on
  every change and re-queued on boot. Best-effort: all I/O is guarded so it
  never affects the render path; inert when the flag is unset.

ops/systemd/studio.service:
- enable STUDIO_PERSIST_QUEUE=1 on the single canonical unit.

scripts/studio-service-swap.sh:
- batch-safe reconcile+apply: waits for an empty queue, removes any stray
  duplicate unit, reinstalls the canonical unit, restarts, health-checks.

docs/ops-crash-and-service.md:
- topology (one canonical unit), how to read crash history, the fix, persistence.

tests-unit/execution_test/prompt_queue_persist_test.py:
- round-trip, inert-when-disabled, and corrupt-row-skipping tests.
2026-07-03 14:42:03 -07:00
hanzo-dev 748e335add studio: native IAM auth + multi-tenant isolation + GPU federation
Auth (middleware/iam_auth_middleware.py): local JWKS JWT validation against
Hanzo IAM (signature + exp + iss + aud=hanzo-studio; org from the `owner`
claim). Standard OIDC Authorization Code flow with PKCE and a /callback
session cookie for the standalone studio.hanzo.ai app; anonymous localhost
bypass unchanged. verify_jwt() extracted as a pure, unit-tested function.

Tenancy (folder_paths.py, app/user_manager.py): make get_public_user_directory
org-rooted so multi-tenant userdata resolves under user/orgs/{org}/... (it
previously always returned None). Scope execution outputs to output/orgs/{org}/
via set_execution_org(), bound by the prompt worker from the queue item org_id.
Also block a token subject from escaping into a System User namespace.

Federation (middleware/{worker_client,prompt_router}.py, server.py): move the
worker registry + execute surface to /v1/workers/register, /v1/workers,
/v1/worker/execute; add X-Worker-Token coordinator trust. Outbound pull channel
for NAT'd local boxes (GB10) specced in docs/federation.md.

Deploy: hanzo.yml + .github/workflows/cicd.yml (hanzoai/ci reusable) build
ghcr.io/hanzoai/studio and roll the studio operator Service CR at
studio.hanzo.ai. PyJWT[crypto] pinned.

Tests: tests-unit/middleware_test/iam_auth_test.py (RS256 sign/verify, state
CSRF, path exemptions) + folder_paths_test/org_scoping_test.py; 140 passing
across the auth/tenancy/user-manager/prompt-server suites.
2026-07-03 14:38:19 -07:00
hanzo-dev 8d74ce9fca model-shot workflows: anoeses-style calm luxury (pure white, front/back poses, pose menu in notes) 2026-07-03 13:20:59 -07:00
hanzo-dev 7fc06c38c9 website catalog architecture: no-body product base + dynamic-model hover variants (8 designs) + workspace seeder
Anoeses-style PDP pattern: product_<design> = garment-only ghost presentation on pure white (base image); hover_<design> = model in dynamic pose on subtle white studio (rollover image). Workspace: index.html seeds Comfy.OpenWorkflowsPaths with all 25 fashion tabs on first load (set-once, localStorage); Comfy.Workflow.Persist on. Seeder added to apply-branding.sh replay path.
2026-07-03 13:12:34 -07:00
hanzo-dev c2f99d072d fashion_product_shot: fix stale model note, point designers at per-design tabs 2026-07-03 12:49:03 -07:00
hanzo-dev e5132a2d7f fashion workflows: per-design shoot_/product_ sets with proven Qwen-Image-Edit-2511 recipe
17 workflows (8 lifestyle shoots + 8 white-seamless product shots + base CAD->lifestyle), each: FluxKontextImageScale 1MP ref, ModelSamplingAuraFlow 3.1, ref-method(index_timestep_zero) on pos+neg conditioning, CFGNorm, EmptySD3Latent 1024x1536, cfg 4.0, randomized seeds, per-design output dirs. UI-lane proof: shot_00004 matches scripted-lane quality (design-exact festival_fuchsia on beach). Rate limiter: default unlimited via STUDIO_RPM env (single-user box).
2026-07-03 12:27:05 -07:00
hanzo-dev ff0dee00a4 studio: add ready-to-run Antje CAD -> lifestyle workflow (Qwen-Image-Edit-2511)
Native TextEncodeQwenImageEditPlus path wired against the live /object_info
schema: LoadImage -> Plus encoder (image1+vae) + VAEEncode -> KSampler
(30 steps, cfg 2.5, euler/simple) -> VAEDecode -> SaveImage swimwear/antje.
Models installed under models/{diffusion_models,text_encoders,vae} (merged
from the Qwen-Image-Edit-2511 diffusers shards, gitignored). 8 of Antje's
CAD drawings preloaded in input/ and enumerated by LoadImage.
2026-07-03 09:10:22 -07:00
hanzo-dev ca9a72f2a7 studio: finish Hanzo rebrand — sidebar mark, black+purple theme, progress favicon
- Sidebar logo: replace the inline ComfyLogo Vue component (top-left mark,
  compiled into the JS bundle, missed by the file-asset pass) with the Hanzo
  mark via branding/patch_sidebar_logo.py (keyed off the stable __name:`ComfyLogo`).
- Theme: branding/hanzo-theme.css forces a pure-black shell + Hanzo-purple
  accents (AA contrast), injected as the last stylesheet; patch_theme.py also
  rewrites the dark palette's canvas clear color to #000000 (CSS can't reach the
  litegraph <canvas>).
- Favicon: make_progress_favicons.py renders the 10 progress frames with the
  Hanzo mark + purple ring, fixing the tab reverting to the Comfy mark during
  generations. Committed PNGs like favicon.ico (Docker needs no rasterizer).
- Tab title: rebrand the runtime ' - ComfyUI' title suffix the string pass missed.
- apply-branding.sh replays all of the above on frontend-package upgrades.
- Rename fork-owned update_comfyui{,_stable}.bat -> update{,_stable}.bat.
2026-07-03 02:20:33 -07:00
hanzo-dev 1ff6280992 Rebrand fork to Hanzo Studio: licensing, branding, Engine nodes, fashion workflows
Licensing (GPL-3.0 preserved):
- NOTICE attributes the ComfyUI upstream and the fork's modifications, GPL-3.0.
- README credits upstream ComfyUI prominently and states GPL-3.0.

Branding (frontend served from the comfyui-frontend-package static dir):
- apply-branding.sh installs the Hanzo favicon (svg + multi-res ico), injects
  icon links, sets <title>Hanzo Studio</title>, and patches display strings.
- make_favicon.py renders the committed favicon.ico from favicon.svg.

Hanzo Engine nodes (custom_nodes/hanzo_engine/, OpenAI-compatible HTTP):
- HanzoChat, HanzoImageGen, HanzoVisionCaption, HanzoSaveText. Graceful node
  errors on connection failure.

Fashion/swimwear starter workflows (user/default/workflows/fashion/):
- fashion_product_shot (FLUX.2 klein t2i), fashion_edit_garment
  (Qwen-Image-Edit i2i), fashion_caption_dataset (Hanzo vision caption loop).
  Each carries a Note node with usage and required files.
2026-07-02 23:17:57 -07:00
z a45a4413dd docs(brand): add hero banner 2026-06-28 20:06:25 -07:00
z 050911912f chore(brand): dynamic hero banner 2026-06-28 20:06:24 -07:00
a025d1236c fix(iam): migrate get-account to OIDC /v1/iam/oauth/userinfo (HIP-0111) (#4)
Co-authored-by: Zach Kelling <z@zeekay.io>
2026-06-24 19:16:12 -07:00
Antje WorringandClaude Opus 4.8 2ecd73d1f0 docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:23:06 -07:00
Hanzo DevandGitHub 7b6b0115e8 ci: migrate to canonical hanzoai/.github/docker-build.yml reusable (#1) 2026-04-23 19:06:49 -07:00
Hanzo AI 3a30e79cfa chore: symlink AGENTS.md and CLAUDE.md to LLM.md
Canonical project context lives in LLM.md. Symlinks ensure
agentic coding tools (Claude Code, Cursor, etc.) find context
automatically regardless of which filename they look for.
2026-04-01 14:14:38 -07:00
Hanzo AI 5354c63777 ci: update GitHub Actions workflows 2026-03-03 14:00:00 -08:00
Hanzo Dev 110139fb28 fix: inline deploy with KMS auth (replace broken reusable workflow ref) 2026-03-12 00:13:02 -07:00
Hanzo Dev c24dee9a5e fix: use secrets inherit for reusable workflow 2026-03-12 00:10:50 -07:00
Hanzo Dev fcc30e1894 fix: use KMS Universal Auth for deploy (replace legacy HANZO_API_KEY) 2026-03-12 00:03:28 -07:00
Hanzo Dev 528f997fc4 ci: migrate deploy to HANZO_API_KEY + KMS via reusable workflow
Replace direct DO_API_TOKEN with reusable-deploy-service.yml from
hanzoai/universe that fetches credentials from KMS using HANZO_API_KEY.
2026-03-11 14:34:53 -07:00
Hanzo Dev 83a9fc6536 docs: add LLM.md project guide 2026-03-11 11:04:48 -07:00
Hanzo Dev bfee9e4b07 Redesign login page to monochrome style matching hanzo.ai
Black background, white text, opacity-based visual hierarchy,
pill-shaped CTA button, Inter font, feature cards with subtle
white/5-10% backgrounds. Removes red accent entirely.
2026-02-23 15:31:30 -08:00
Hanzo Dev dafd40026c Fix lint: remove unused imports in main.py and billing_middleware.py 2026-02-23 15:21:20 -08:00
Hanzo Dev ece384a3d0 Add GPU worker system, compute config API, and login gate
- Compute profiles: per-org CPU/GPU config stored in user dir with
  thread-safe CRUD and atomic file writes
- Prompt routing: forwards GPU prompts to registered workers, falls
  back to local execution when no worker available
- Worker mode: --worker-mode flag runs Studio as headless executor
  that registers with coordinator via heartbeats
- Visor integration: provision/terminate GPU VMs on AWS (T4/V100/A100/H100)
  with shell-safe startup scripts
- Login page: unauthenticated browsers see branded login gate with
  "Sign in with Hanzo" button instead of broken ComfyUI interface
- 6 new API endpoints under /api/compute/*
2026-02-23 15:17:59 -08:00
Hanzo Dev c43cab93ba Fix metrics endpoint content_type for aiohttp compatibility 2026-02-23 14:31:07 -08:00
Hanzo Dev 91654b973a Add metrics, billing, and rate limiting middleware
- /metrics endpoint (Prometheus text format) for VictoriaMetrics scraping
- Commerce billing integration: usage recording after prompt execution,
  balance check before accepting prompts (402 Payment Required)
- Per-org rate limiting on prompt submissions (default 60 rpm)
- All features toggled via STUDIO_* env vars for K8s deployment
- Metrics tracks: prompts total/active, queue depth, websocket connections,
  execution duration histogram, HTTP request counters
2026-02-23 14:17:24 -08:00
Hanzo Dev 8c9652bb78 Fix P0 production readiness issues
- Add /health and /ready endpoints for K8s probes
- Add SIGTERM graceful shutdown handler in main.py
- Fix IAM middleware: shared aiohttp session (not per-request),
  60s token validation cache, stale entry eviction
- Remove /api/system_stats from unauthenticated public paths
- Update .dockerignore to exclude tests, scripts, temp data
2026-02-23 13:17:32 -08:00
Hanzo Dev eb5935be5f Exclude build scripts from ruff linting
scripts/ and branding/ are build-time utilities, not runtime code.
2026-02-23 13:10:59 -08:00
Hanzo Dev 79f0bf36a1 Add IAM auth, multi-tenant storage, and fix frontend branding
- Fix branding patcher to target actual upstream logo filenames
  (comfy-logo-*.svg, not studio-logo-*.svg)
- Add more display string patterns to patch_frontend.py
- Add IAM authentication middleware (hanzo.id/api/get-account)
  with localhost bypass and browser login redirect
- Add multi-tenant org-scoped storage (folder_paths.get_org_*)
  with per-org output/input/temp/user/models directories
- Add CLI flags: --enable-iam-auth, --iam-url, --multi-tenant,
  --org-id, --no-localhost-bypass
- Support env var toggles for Docker: STUDIO_ENABLE_IAM_AUTH,
  STUDIO_MULTI_TENANT, STUDIO_ORG_ID, STUDIO_IAM_URL
- Integrate IAM user context into UserManager and server routes
2026-02-23 13:00:26 -08:00
Hanzo Dev 5cf6ce2508 Rename all internal modules: comfy* → studio*
Complete module rename across 552 files:
- comfy/ → studio/
- comfy_extras/ → studio_extras/
- comfy_api/ → studio_api/
- comfy_api_nodes/ → studio_api_nodes/
- comfy_config/ → studio_config/
- comfy_execution/ → studio_execution/
- comfy/comfy_types/ → studio/node_types/
- comfyui_version.py → studio_version.py
- All test directories renamed
- All import statements updated
- All internal identifiers renamed (ComfyNodeABC → StudioNodeABC, etc.)
- All wire protocol types renamed (COMFY_* → STUDIO_*)
- No backward compatibility shims — clean break
- External pip packages preserved (comfyui-frontend-package, comfy-kitchen, etc.)
- 518 Python files syntax-validated, 0 errors
2026-02-23 12:49:38 -08:00
Hanzo Dev fdb4b5aab8 Complete Hanzo Studio rebrand: purge all remaining ComfyUI references
99 files changed across the entire codebase:
- CLI help strings, error messages, and display text
- README.md fully rewritten for Hanzo Studio
- CONTRIBUTING.md, CODEOWNERS, issue templates updated
- All GitHub workflow references updated
- Default output filename prefixes (ComfyUI -> HanzoStudio)
- .ci/ batch files and READMEs for Windows portable builds
- Core file headers ("This file is part of Hanzo Studio")
- API docs URLs (docs.comfy.org -> docs.hanzo.ai)
- API base URL default (api.comfy.org -> api.hanzo.ai)
- comfy_extras node descriptions and default paths
- Test file comments and docstrings
- extra_model_paths.yaml.example config comments

Preserved as-is (would break imports/packages):
- Python package names (comfyui_frontend_package, comfyui_manager, etc.)
- Internal module paths (comfy/, comfy_extras/, comfy_api/, etc.)
- API contract values (AUTH_TOKEN_COMFY_ORG, etc.)
- Third-party source attribution URLs
- custom_nodes/ directory contents
2026-02-23 10:13:55 -08:00
Hanzo Dev 4f0ddab1b8 Refine branding patcher: remove catch-all Comfy-Org replacement
- Remove bare 'Comfy-Org' and 'ComfyOrg' from plain text replacements
  (was corrupting i18n keys like OpenComfy-OrgDiscord)
- Only replace Comfy-Org/ComfyOrg in multi-word display phrases
- Add regex patterns for quoted string value contexts
- Add standalone quoted value patterns ("ComfyUI" → "Hanzo Studio")
- Add Comfy Cloud → Hanzo Cloud display text replacement
2026-02-23 09:51:59 -08:00
Hanzo Dev ee7da07a4e Fix branding: use smart Python patcher instead of blind sed
The previous sed-based approach broke JavaScript by replacing code
identifiers (class names, import paths, i18n keys) alongside display
strings. The new Python patcher:

- Only replaces display strings within quoted contexts
- Safely replaces all URLs (comfy.org → hanzo.ai, Discord, GitHub)
- Preserves JS class names (ComfyUI, ComfyApp, etc.)
- Preserves CSS class names (comfyui-button, etc.)
- Preserves dynamic import paths (./ComfyOrgHeader-*.js)
- Preserves i18n keys while updating i18n values
- Recursively patches all files including extensions/
2026-02-23 09:46:46 -08:00
Hanzo Dev 4526acbfa6 Comprehensive rebrand: ComfyUI → Hanzo Studio
- Replace all user-facing "ComfyUI" strings with "Hanzo Studio"
- Update Help menu links: Discord → discord.gg/hanzoai, GitHub → hanzoai/studio
- Replace comfy.org URLs with hanzo.ai equivalents
- Update support email to support@hanzo.ai
- Rewrite branding script to patch ALL JS bundles comprehensively
- Update default filename prefixes from ComfyUI to HanzoStudio
- Fix CI to trigger on main branch (not hanzo/main)
- Update pyproject.toml metadata (name, URLs)
2026-02-23 09:32:01 -08:00
Hanzo Dev ee087ae50e Rebrand ComfyUI to Hanzo Studio
- Replace ComfyUI logo with Hanzo geometric H mark
- Replace favicon with Hanzo H favicon
- Patch index.html title, PWA manifest, and JS bundles
- Update AboutPanel and OrgHeader references
- Branding applied as Docker build step after pip install
2026-02-23 09:21:18 -08:00
Hanzo Dev e6873292a6 Add --cpu flag to ComfyUI startup for non-CUDA environments
ComfyUI's model_management crashes on startup without CUDA unless
--cpu flag is passed. GPU deployments will override CMD.
2026-02-23 01:54:06 -08:00
Hanzo Dev 54b02fac6c Fix Dockerfile: libgl1-mesa-glx obsoleted in Debian Trixie, use libgl1 2026-02-23 01:38:56 -08:00
Hanzo Dev cd55589499 Fix CI trigger branch to hanzo/main 2026-02-23 01:37:48 -08:00
Hanzo Dev d816711e01 Add Dockerfile, CI/CD, and .dockerignore for Hanzo Studio
ComfyUI deployment via GHCR + K8s on hanzo-k8s cluster.
CPU-only base image, GPU support via nvidia runtime at deploy.
2026-02-23 01:36:42 -08:00
comfyanonymousandGitHub f7a591ffa2 Fix issue loading fp8 ltxav checkpoints. (#12582) 2026-02-22 16:00:02 -05:00
comfyanonymousandGitHub 4e31960261 Fix dtype issue in embeddings connector. (#12570) 2026-02-22 03:18:20 -05:00
comfyanonymousandGitHub dd19d1b91a Move LTXAV av embedding connectors to diffusion model. (#12569) 2026-02-21 22:29:58 -05:00
Christian ByrneandGitHub 2c8d4e628f chore: tune CodeRabbit config to limit review scope and disable for drafts (#12567)
* chore: tune CodeRabbit config to limit review scope and disable for drafts

- Add tone_instructions to focus only on newly introduced issues
- Add global path_instructions entry to ignore pre-existing issues in moved/reformatted code
- Disable draft PR reviews (drafts: false) and add WIP title keywords
- Disable ruff tool to prevent linter-based outside-diff-range comments

Addresses feedback from maintainers about CodeRabbit flagging pre-existing
issues in code that was merely moved or de-indented (e.g., PR #12557),
which can discourage community contributions and cause scope creep.

Amp-Thread-ID: https://ampcode.com/threads/T-019c82de-0481-7253-ad42-20cb595bb1ba

* chore: add 'DO NOT MERGE' to ignore_title_keywords

Amp-Thread-ID: https://ampcode.com/threads/T-019c82de-0481-7253-ad42-20cb595bb1ba
2026-02-21 18:32:15 -08:00
Christian ByrneandGitHub 9668d4d48b Add category to Normalized Attention Guidance node (#12565) 2026-02-21 19:51:21 -05:00
664f312d50 fix: specify UTF-8 encoding when reading subgraph files (#12563)
On Windows, Python defaults to cp1252 encoding when no encoding is
specified. JSON files containing UTF-8 characters (e.g., non-ASCII
characters) cause UnicodeDecodeError when read with cp1252.

This fixes the error that occurs when loading blueprint subgraphs
on Windows systems.

https://claude.ai/code/session_014WHi3SL9Gzsi3U6kbSjbSb

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 15:05:00 -08:00
rattusandGitHub a8eac8219c comfy-aimdo 0.2 - Improved pytorch allocator integration (#12557)
Integrate comfy-aimdo 0.2 which takes a different approach to
installing the memory allocator hook. Instead of using the complicated
and buggy pytorch MemPool+CudaPluggableAlloctor, cuda is directly hooked
making the process much more transparent to both comfy and pytorch. As
far as pytorch knows, aimdo doesnt exist anymore, and just operates
behind the scenes.

Remove all the mempool setup stuff for dynamic_vram and bump the
comfy-aimdo version. Remove the allocator object from memory_management
and demote its use as an enablment check to a boolean flag.

Comfy-aimdo 0.2 also support the pytorch cuda async allocator, so
remove the dynamic_vram based force disablement of cuda_malloc and
just go back to the old settings of allocators based on command line
input.
2026-02-21 10:52:57 -08:00
pythongosssssandGitHub b777ae17bc add support for pyopengl < 3.1.4 where the size parameter does not exist (#12555) 2026-02-21 06:14:57 -08:00
9e4b3e8cd6 fix: swap essentials_category from CLIPTextEncode to PrimitiveStringMultiline (#12553)
Remove CLIPTextEncode from Basics essentials category and add
PrimitiveStringMultiline (String Multiline) in its place.

Amp-Thread-ID: https://ampcode.com/threads/T-019c7efb-d916-7244-8c43-77b615ba0622

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-20 23:46:46 -08:00
Christian ByrneandGitHub 0ffa3f22ca feat: add essential subgraph blueprints (#12552)
Add 24 non-cloud essential blueprints from comfyui-wiki/Subgraph-Blueprints.
These cover common workflows: text/image/video generation, editing,
inpainting, outpainting, upscaling, depth maps, pose, captioning, and more.

Cloud-only blueprints (5) are excluded and will be added once
client-side distribution filtering lands.

Amp-Thread-ID: https://ampcode.com/threads/T-019c6f43-6212-7308-bea6-bfc35a486cbf
2026-02-20 23:40:13 -08:00
2b512fd559 update glsl blueprint with gradient (#12548)
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-20 23:02:36 -08:00
4f2e01051c feat: add gradient-slider display mode for FLOAT inputs (#12536)
* feat: add gradient-slider display mode for FLOAT inputs

* fix: use precise type annotation list[list[float]] for gradient_stops

Amp-Thread-ID: https://ampcode.com/threads/T-019c7eea-be2b-72ce-a51f-838376f9b7a7

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
2026-02-20 22:52:32 -08:00
Arthur R LongbottomandGitHub 9677992b83 Fix non-contiguous audio waveform crash in video save (#12550)
Fixes #12549
2026-02-20 23:37:55 -05:00
comfyanonymousandGitHub 3ca8a3c278 Update nightly installation command for ROCm (#12547) 2026-02-20 21:32:05 -05:00
b558b8343a chore: add CodeRabbit configuration for automated code review (#12539)
Amp-Thread-ID: https://ampcode.com/threads/T-019c7915-2abf-743c-9c74-b93d87d63055

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-20 00:25:54 -08:00
619ff184a4 [API Nodes] add ElevenLabs nodes (#12207)
* feat(api-nodes): add ElevenLabs API nodes

* added price badge for ElevenLabsInstantVoiceClone node

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 22:12:28 -08:00
ca26c5c104 Add GLSL shader node using PyOpenGL (#12148)
* adds support for executing simple glsl shaders
using moderngl package

* tidy

* Support multiple outputs

* Try fix build

* fix casing

* fix line endings

* convert to using PyOpenGL and glfw

* remove cpu support

* tidy

* add additional support for egl & osmesa backends

* fix ci
perf: only read required outputs

* add diagnostics, update mac initialization

* GLSL glueprints + node fixes (#12492)

* Add image operation blueprints

* Add channels

* Add glow

* brightness/contrast

* hsb

* add glsl shader update system

* shader nit iteration

* add multipass for faster blur

* more fixes

* rebuild blueprints

* print -> logger

* Add edge preserving blur

* fix: move _initialized flag to end of GLContext.__init__

Prevents '_vao' attribute error when init fails partway through
and subsequent calls skip initialization due to early _initialized flag.

* update valid ranges
- threshold 0-100
- step 0+

* fix value ranges

* rebuild node to remove extra inputs

* Fix gamma step

* clamp saturation in colorize instead of wrapping

* Fix crash on 1x1 px images

* rework description

* remove unnecessary f


Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: Hunter Senft-Grupp <hunter@comfy.org>
2026-02-19 23:22:13 -05:00
comfyanonymousandGitHub 287981e1c3 Force min length 1 when tokenizing for text generation. (#12538) 2026-02-19 22:57:44 -05:00
comfyanonymousandGitHub a73a5974b1 Small cleanup and try to get qwen 3 work with the text gen. (#12537) 2026-02-19 22:42:28 -05:00
47585f08c0 feat: mark 429 widgets as advanced for collapsible UI (#12197)
* feat: mark 429 widgets as advanced for collapsible UI

Mark widgets as advanced across core, comfy_extras, and comfy_api_nodes
to support the new collapsible advanced inputs section in the frontend.

Changes:
- 267 advanced markers in comfy_extras/
- 162 advanced markers in comfy_api_nodes/
- All files pass python3 -m py_compile verification

Widgets marked advanced (hidden by default):
- Scheduler internals: sigma_max, sigma_min, rho, mu, beta, alpha
- Sampler internals: eta, s_noise, order, rtol, atol, h_init, pcoeff, etc.
- Memory optimization: tile_size, overlap, temporal_size, temporal_overlap
- Pipeline controls: add_noise, start_at_step, end_at_step
- Timing controls: start_percent, end_percent
- Layer selection: stop_at_clip_layer, layers, block_number
- Video encoding: codec, crf, format
- Device/dtype: device, noise_device, dtype, weight_dtype

Widgets kept basic (always visible):
- Core params: strength, steps, cfg, denoise, seed, width, height
- Model selectors: ckpt_name, lora_name, vae_name, sampler_name
- Common controls: upscale_method, crop, batch_size, fps, opacity

Related: frontend PR #11939
Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc

* fix: remove advanced=True from DynamicCombo.Input (unsupported)

Amp-Thread-ID: https://ampcode.com/threads/T-019c1734-6b61-702e-b333-f02c399963fc

* fix: address review - un-mark model merge, video, image, and training node widgets as advanced

Per comfyanonymous review:
- Model merge arguments should not be advanced (all 14 model-specific merge classes)
- SaveAnimatedWEBP lossless/quality/method should not be advanced
- SaveWEBM/SaveVideo codec/crf/format should not be advanced
- TrainLoraNode options should not be advanced (7 inputs)

Amp-Thread-ID: https://ampcode.com/threads/T-019c322b-a3a8-71b7-9962-d44573ca6352

* fix: un-mark batch_size and webcam width/height as advanced (should stay basic)

Amp-Thread-ID: https://ampcode.com/threads/T-019c3236-1417-74aa-82a3-bcb365fbe9d1

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:20:02 -08:00
3e33a7508c feat: add essentials_category (#12357)
* feat: add essentials_category field to node schema

Amp-Thread-ID: https://ampcode.com/threads/T-019c2b25-cd90-7218-9071-03cb46b351b3

* feat: add ESSENTIALS_CATEGORY to core nodes

Marked nodes:
- Basic: LoadImage, SaveImage, LoadVideo, SaveVideo, Load3D, CLIPTextEncode
- Image Tools: ImageScale, ImageInvert, ImageBatch, ImageCrop, ImageRotate, ImageBlur
- Image Tools/Preprocessing: Canny
- Image Generation: LoraLoader
- Audio: LoadAudio, SaveAudio

Amp-Thread-ID: https://ampcode.com/threads/T-019c2b25-cd90-7218-9071-03cb46b351b3

* Add ESSENTIALS_CATEGORY to more nodes

- SaveGLB (Basic)
- GetVideoComponents (Video Tools)
- TencentTextToModelNode, TencentImageToModelNode (3D)
- RecraftRemoveBackgroundNode (Image Tools)
- KlingLipSyncAudioToVideoNode (Video Generation)
- OpenAIChatNode (Text Generation)
- StabilityTextToAudio (Audio)

Amp-Thread-ID: https://ampcode.com/threads/T-019c2b69-81c1-71c3-8096-450a39e20910

* fix: correct essentials category for Canny node

Amp-Thread-ID: https://ampcode.com/threads/T-019c7303-ab53-7341-be76-a5da1f7a657e
Co-authored-by: Amp <amp@ampcode.com>

* refactor: replace essentials_category string literals with constants

Amp-Thread-ID: https://ampcode.com/threads/T-019c7303-ab53-7341-be76-a5da1f7a657e
Co-authored-by: Amp <amp@ampcode.com>

* refactor: revert constants, use string literals for essentials_category

Amp-Thread-ID: https://ampcode.com/threads/T-019c7303-ab53-7341-be76-a5da1f7a657e
Co-authored-by: Amp <amp@ampcode.com>

* fix: update basics

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-19 19:00:26 -08:00
Alexander PiskunandGitHub f9da0e53be fix(api-nodes): force Gemini to return uncompressed images (#12516) 2026-02-19 04:10:39 -08:00
Jukka SeppänenandGitHub d3771ede35 feat: Add basic text generation support with native models, initially supporting Gemma3 (#12392) 2026-02-18 20:49:43 -05:00
comfyanonymousandGitHub 402c9bbeab Add simple 3 band equalizer node for audio. (#12519) 2026-02-18 18:36:35 -05:00
Alexander PiskunandGitHub 6ead1c10d3 fix(api-nodes): add price badge for Rodin Gen-2 node (#12512) 2026-02-17 23:15:23 -08:00
HunterandGitHub 8ee1cb2bdc fix: use glob matching for Gemini image MIME types (#12511)
gemini-3-pro-image-preview nondeterministically returns image/jpeg
instead of image/png. get_image_from_response() hardcoded
get_parts_by_type(response, "image/png"), silently dropping JPEG
responses and falling back to torch.zeros (all-black output).

Add _mime_matches() helper using fnmatch for glob-style MIME matching.
Change get_image_from_response() to request "image/*" so any image
format returned by the API is correctly captured.
2026-02-18 00:03:54 -05:00
1e5c7e99a1 BBox widget (#11594)
* Boundingbox widget

* code improve

---------

Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-02-17 17:13:39 -08:00
f3bf4ec617 Bump comfyui-frontend-package to 1.39.14 (#12494)
* Bump comfyui-frontend-package to 1.39.13

* Update requirements.txt

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-02-17 13:41:34 -08:00
rattusandGitHub 3f59f0576e ops: limit return of requants (#12506)
This check was far too broad and the dtype is not a reliable indicator
of wanting the requant (as QT returns the compute dtype as the dtype).
So explictly plumb whether fp8mm wants the requant or not.
2026-02-17 15:32:27 -05:00
comfyanonymous c5ba0ada07 ComfyUI v0.14.1 2026-02-17 13:28:06 -05:00
ComfyUI WikiandGitHub 696a980bf4 chore: update workflow templates to v0.8.43 (#12507) 2026-02-17 13:25:55 -05:00
Alexander PiskunandGitHub 270582a5af feat(api-nodes): add Recraft V4 nodes (#12502) 2026-02-17 13:25:44 -05:00
Alexander PiskunandGitHub 1b0600eba3 feat(api-nodes): add "viduq3-turbo" model and Vidu3StartEnd node; fix the price badges (#12482) 2026-02-17 10:07:14 -08:00
chaObservandGitHub 169bc124a7 Fix anima LLM adapter forward when manual cast (#12504) 2026-02-17 07:56:44 -08:00
comfyanonymous 0de6f548d2 ComfyUI v0.14.0 2026-02-17 00:39:54 -05:00
comfyanonymousandGitHub bc072f111d Fix anima preprocess text embeds not using right inference dtype. (#12501) 2026-02-17 00:29:20 -05:00
comfyanonymousandGitHub 9ff688ba3b Implement NAG on all the models based on the Flux code. (#12500)
Use the Normalized Attention Guidance node.

Flux, Flux2, Klein, Chroma, Chroma radiance, Hunyuan Video, etc..
2026-02-16 23:30:34 -05:00
Jedrzej KosinskiandGitHub 6072e457f1 Allow control_after_generate to be type ControlAfterGenerate in v3 schema (#12187) 2026-02-16 22:20:21 -05:00
Alex ButlerandGitHub e61e047bcf add venv* to gitignore (#12431) 2026-02-16 22:16:19 -05:00
comfyanonymousandGitHub 4836097ab7 Remove code to support RMSNorm on old pytorch. (#12499) 2026-02-16 20:09:24 -05:00
ComfyUI WikiandGitHub 1c540d6337 chore: update workflow templates to v0.8.42 (#12491) 2026-02-16 17:33:43 -05:00
comfyanonymousandGitHub 74cb3d8dd0 Remove workaround for old pytorch. (#12480) 2026-02-15 20:43:53 -05:00
rattusandGitHub 84377afb49 MPDynamic: force load flux img_in weight (Fixes flux1 canny+depth lora crash) (#12446)
* lora: add weight shape calculations.

This lets the loader know if a lora will change the shape of a weight
so it can take appropriate action.

* MPDynamic: force load flux img_in weight

This weight is a bit special, in that the lora changes its geometry.
This is rather unique, not handled by existing estimate and doesn't
work for either offloading or dynamic_vram.

Fix for dynamic_vram as a special case. Ideally we can fully precalculate
these lora geometry changes at load time, but just get these models
working first.
2026-02-15 20:30:09 -05:00
rattusandGitHub 1a37669cbb Fix lora Extraction in offload conditions (+ dynamic_vram mode) (#12479)
* lora_extract: Add a trange

If you bite off more than your GPU can chew, this kinda just hangs.
Give a rough indication of progress counting the weights in a trange.

* lora_extract: Support on-the-fly patching

Use the on-the-fly approach from the regular model saving logic for
lora extraction too. Switch off force_cast_weights accordingly.

This gets extraction working in dynamic vram while also supporting
extraction on GPU offloaded.
2026-02-15 20:28:51 -05:00
9611835ecf feat(api-nodes): add Bria RMBG nodes (#12465)
Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com>
2026-02-15 17:22:30 -08:00
Alexander PiskunandGitHub 6e670fdc84 feat(api-nodes-Tencent): add ModelTo3DUV, 3DTextureEdit, 3DParts nodes (#12428)
* feat(api-nodes-Tencent): add ModelTo3DUV, 3DTextureEdit, 3DParts nodes

* add image output to TencentModelTo3DUV node

* commented out two nodes

* added rate_limit check to other hunyuan3d nodes
2026-02-15 05:33:18 -08:00
Jedrzej KosinskiandGitHub 5002b9e613 Node Replacement API (#12014) 2026-02-15 02:12:30 -08:00
Alexander PiskunandGitHub 6d9adf9174 chore(api-nodes): remove "gpt-4o" model (#12467) 2026-02-15 01:31:59 -08:00
comfyanonymousandGitHub 73ebbc1eff Remove unsafe pickle loading code that was used on pytorch older than 2.4 (#12473)
ComfyUI hasn't started on pytorch 2.4 since last month.
2026-02-14 22:53:52 -05:00
Christian ByrneandGitHub 7636ac692d Update frontend package to 1.38.14 (#12469) 2026-02-14 11:01:10 -08:00
780 changed files with 40472 additions and 55615 deletions
+8
View File
@@ -0,0 +1,8 @@
@echo off
..\python_embeded\python.exe .\update.py ..\HanzoStudio\
if exist update_new.py (
move /y update_new.py update.py
echo Running updater again since it got updated.
..\python_embeded\python.exe .\update.py ..\HanzoStudio\ --skip_self_update
)
if "%~1"=="" pause
+3 -3
View File
@@ -47,7 +47,7 @@ def pull(repo, remote_name='origin', branch='master'):
pygit2.option(pygit2.GIT_OPT_SET_OWNER_VALIDATION, 0)
repo_path = str(sys.argv[1])
repo = pygit2.Repository(repo_path)
ident = pygit2.Signature('comfyui', 'comfy@ui')
ident = pygit2.Signature('hanzo-studio', 'studio@hanzo.ai')
try:
print("stashing current changes") # noqa: T201
repo.stash(ident)
@@ -153,8 +153,8 @@ if not os.path.exists(req_path) or not files_equal(repo_req_path, req_path):
pass
stable_update_script = os.path.join(repo_path, ".ci/update_windows/update_comfyui_stable.bat")
stable_update_script_to = os.path.join(cur_path, "update_comfyui_stable.bat")
stable_update_script = os.path.join(repo_path, ".ci/update_windows/update_studio_stable.bat")
stable_update_script_to = os.path.join(cur_path, "update_studio_stable.bat")
try:
if not file_size(stable_update_script_to) > 10:
-8
View File
@@ -1,8 +0,0 @@
@echo off
..\python_embeded\python.exe .\update.py ..\ComfyUI\
if exist update_new.py (
move /y update_new.py update.py
echo Running updater again since it got updated.
..\python_embeded\python.exe .\update.py ..\ComfyUI\ --skip_self_update
)
if "%~1"=="" pause
@@ -1,8 +0,0 @@
@echo off
..\python_embeded\python.exe .\update.py ..\ComfyUI\ --stable
if exist update_new.py (
move /y update_new.py update.py
echo Running updater again since it got updated.
..\python_embeded\python.exe .\update.py ..\ComfyUI\ --skip_self_update --stable
)
if "%~1"=="" pause
+8
View File
@@ -0,0 +1,8 @@
@echo off
..\python_embeded\python.exe .\update.py ..\HanzoStudio\ --stable
if exist update_new.py (
move /y update_new.py update.py
echo Running updater again since it got updated.
..\python_embeded\python.exe .\update.py ..\HanzoStudio\ --skip_self_update --stable
)
if "%~1"=="" pause
@@ -7,22 +7,19 @@ If you have a AMD gpu:
run_amd_gpu.bat
If you have memory issues you can try disabling the smart memory management by running comfyui with:
If you have memory issues you can try disabling the smart memory management by running Hanzo Studio with:
run_amd_gpu_disable_smart_memory.bat
IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: ComfyUI\models\checkpoints
IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: HanzoStudio\models\checkpoints
You can download the stable diffusion XL one from: https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/sd_xl_base_1.0_0.9vae.safetensors
RECOMMENDED WAY TO UPDATE:
To update the ComfyUI code: update\update_comfyui.bat
To update the Hanzo Studio code: update\update_studio.bat
TO SHARE MODELS BETWEEN COMFYUI AND ANOTHER UI:
In the ComfyUI directory you will find a file: extra_model_paths.yaml.example
TO SHARE MODELS BETWEEN HANZO STUDIO AND ANOTHER UI:
In the HanzoStudio directory you will find a file: extra_model_paths.yaml.example
Rename this file to: extra_model_paths.yaml and edit it with your favorite text editor.
+1 -1
View File
@@ -1,2 +1,2 @@
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build
.\python_embeded\python.exe -s HanzoStudio\main.py --windows-standalone-build
pause
@@ -1,2 +1,2 @@
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --disable-smart-memory
.\python_embeded\python.exe -s HanzoStudio\main.py --windows-standalone-build --disable-smart-memory
pause
@@ -1,2 +1,2 @@
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --fast
.\python_embeded\python.exe -s HanzoStudio\main.py --windows-standalone-build --fast
pause
@@ -15,20 +15,20 @@ run_cpu.bat
IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: ComfyUI\models\checkpoints
IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: HanzoStudio\models\checkpoints
You can download the stable diffusion 1.5 one from: https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/blob/main/v1-5-pruned-emaonly-fp16.safetensors
You can download the stable diffusion 1.5 one from: https://huggingface.co/hanzoai/stable-diffusion-v1-5-archive/blob/main/v1-5-pruned-emaonly-fp16.safetensors
RECOMMENDED WAY TO UPDATE:
To update the ComfyUI code: update\update_comfyui.bat
To update the Hanzo Studio code: update\update_studio.bat
To update ComfyUI with the python dependencies, note that you should ONLY run this if you have issues with python dependencies.
update\update_comfyui_and_python_dependencies.bat
To update Hanzo Studio with the python dependencies, note that you should ONLY run this if you have issues with python dependencies.
update\update_studio_and_python_dependencies.bat
TO SHARE MODELS BETWEEN COMFYUI AND ANOTHER UI:
In the ComfyUI directory you will find a file: extra_model_paths.yaml.example
TO SHARE MODELS BETWEEN HANZO STUDIO AND ANOTHER UI:
In the HanzoStudio directory you will find a file: extra_model_paths.yaml.example
Rename this file to: extra_model_paths.yaml and edit it with your favorite text editor.
@@ -1,3 +1,3 @@
..\python_embeded\python.exe -s ..\ComfyUI\main.py --windows-standalone-build --disable-api-nodes
echo If you see this and ComfyUI did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
..\python_embeded\python.exe -s ..\HanzoStudio\main.py --windows-standalone-build --disable-api-nodes
echo If you see this and Hanzo Studio did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
pause
+1 -1
View File
@@ -1,2 +1,2 @@
.\python_embeded\python.exe -s ComfyUI\main.py --cpu --windows-standalone-build
.\python_embeded\python.exe -s HanzoStudio\main.py --cpu --windows-standalone-build
pause
@@ -1,3 +1,3 @@
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build
echo If you see this and ComfyUI did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
.\python_embeded\python.exe -s HanzoStudio\main.py --windows-standalone-build
echo If you see this and Hanzo Studio did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
pause
@@ -1,3 +1,3 @@
.\python_embeded\python.exe -s ComfyUI\main.py --windows-standalone-build --fast fp16_accumulation
echo If you see this and ComfyUI did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
.\python_embeded\python.exe -s HanzoStudio\main.py --windows-standalone-build --fast fp16_accumulation
echo If you see this and Hanzo Studio did not start try updating your Nvidia Drivers to the latest. If you get a c10.dll error you need to install vc redist that you can find: https://aka.ms/vc14/vc_redist.x64.exe
pause
+127
View File
@@ -0,0 +1,127 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
early_access: false
tone_instructions: "Only comment on issues introduced by this PR's changes. Do not flag pre-existing problems in moved, re-indented, or reformatted code."
reviews:
profile: "chill"
request_changes_workflow: false
high_level_summary: false
poem: false
review_status: false
review_details: false
commit_status: true
collapse_walkthrough: true
changed_files_summary: false
sequence_diagrams: false
estimate_code_review_effort: false
assess_linked_issues: false
related_issues: false
related_prs: false
suggested_labels: false
auto_apply_labels: false
suggested_reviewers: false
auto_assign_reviewers: false
in_progress_fortune: false
enable_prompt_for_ai_agents: true
path_filters:
- "!studio_api_nodes/apis/**"
- "!**/generated/*.pyi"
- "!.ci/**"
- "!script_examples/**"
- "!**/__pycache__/**"
- "!**/*.ipynb"
- "!**/*.png"
- "!**/*.bat"
path_instructions:
- path: "**"
instructions: |
IMPORTANT: Only comment on issues directly introduced by this PR's code changes.
Do NOT flag pre-existing issues in code that was merely moved, re-indented,
de-indented, or reformatted without logic changes. If code appears in the diff
only due to whitespace or structural reformatting (e.g., removing a `with:` block),
treat it as unchanged. Contributors should not feel obligated to address
pre-existing issues outside the scope of their contribution.
- path: "studio/**"
instructions: |
Core ML/diffusion engine. Focus on:
- Backward compatibility (breaking changes affect all custom nodes)
- Memory management and GPU resource handling
- Performance implications in hot paths
- Thread safety for concurrent execution
- path: "studio_api_nodes/**"
instructions: |
Third-party API integration nodes. Focus on:
- No hardcoded API keys or secrets
- Proper error handling for API failures (timeouts, rate limits, auth errors)
- Correct Pydantic model usage
- Security of user data passed to external APIs
- path: "studio_extras/**"
instructions: |
Community-contributed extra nodes. Focus on:
- Consistency with node patterns (INPUT_TYPES, RETURN_TYPES, FUNCTION, CATEGORY)
- No breaking changes to existing node interfaces
- path: "studio_execution/**"
instructions: |
Execution engine (graph execution, caching, jobs). Focus on:
- Caching correctness
- Concurrent execution safety
- Graph validation edge cases
- path: "nodes.py"
instructions: |
Core node definitions (2500+ lines). Focus on:
- Backward compatibility of NODE_CLASS_MAPPINGS
- Consistency of INPUT_TYPES return format
- path: "alembic_db/**"
instructions: |
Database migrations. Focus on:
- Migration safety and rollback support
- Data preservation during schema changes
auto_review:
enabled: true
auto_incremental_review: true
drafts: false
ignore_title_keywords:
- "WIP"
- "DO NOT REVIEW"
- "DO NOT MERGE"
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: false
tools:
ruff:
enabled: false
pylint:
enabled: false
flake8:
enabled: false
gitleaks:
enabled: true
shellcheck:
enabled: false
markdownlint:
enabled: false
yamllint:
enabled: false
languagetool:
enabled: false
github-checks:
enabled: true
timeout_ms: 90000
ast-grep:
essential_rules: true
chat:
auto_reply: true
knowledge_base:
opt_out: false
learnings:
scope: "auto"
+33
View File
@@ -0,0 +1,33 @@
models/
output/
input/
custom_nodes/*
# ...but the pack manifest must reach the build context so the image can vendor
# the packs reproducibly (scripts/install_custom_nodes.sh clones them at build).
# Local clones stay excluded so the image builds them fresh from the manifest.
!custom_nodes/hanzo-packs.txt
user/
temp/
orgs/
.git/
.github/
__pycache__/
*.pyc
*.pyo
*.log
*.egg-info/
.venv/
venv/
.mypy_cache/
.pytest_cache/
.ruff_cache/
tests/
tests-unit/
# scripts/ ships WHOLE: the image build runs scripts/install_custom_nodes.sh and
# the studio pod runs scripts/library_manifest.py as the build-manifest sidecar
# (indexes every org's output/ into library.json). Excluding it silently no-ops
# the sidecar's `[ -f ]` guard, so nothing gets indexed — keep the tree intact.
notebooks/
*.swp
.env
.env.*
+7 -7
View File
@@ -1,5 +1,5 @@
name: Bug Report
description: "Something is broken inside of ComfyUI. (Do not use this if you're just having issues and need help, or if the issue relates to a custom node)"
description: "Something is broken inside of Hanzo Studio. (Do not use this if you're just having issues and need help, or if the issue relates to a custom node)"
labels: ["Potential Bug"]
body:
- type: markdown
@@ -7,23 +7,23 @@ body:
value: |
Before submitting a **Bug Report**, please ensure the following:
- **1:** You are running the latest version of ComfyUI.
- **2:** You have your ComfyUI logs and relevant workflow on hand and will post them in this bug report.
- **1:** You are running the latest version of Hanzo Studio.
- **2:** You have your Hanzo Studio logs and relevant workflow on hand and will post them in this bug report.
- **3:** You confirmed that the bug is not caused by a custom node. You can disable all custom nodes by passing
`--disable-all-custom-nodes` command line argument. If you have custom node try updating them to the latest version.
- **4:** This is an actual bug in ComfyUI, not just a support question. A bug is when you can specify exact
- **4:** This is an actual bug in Hanzo Studio, not just a support question. A bug is when you can specify exact
steps to replicate what went wrong and others will be able to repeat your steps and see the same issue happen.
## Very Important
Please make sure that you post ALL your ComfyUI logs in the bug report. A bug report without logs will likely be ignored.
Please make sure that you post ALL your Hanzo Studio logs in the bug report. A bug report without logs will likely be ignored.
- type: checkboxes
id: custom-nodes-test
attributes:
label: Custom Node Testing
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
options:
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.hanzo.ai/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
required: false
- type: textarea
attributes:
@@ -40,7 +40,7 @@ body:
- type: textarea
attributes:
label: Steps to Reproduce
description: "Describe how to reproduce the issue. Please be sure to attach a workflow JSON or PNG, ideally one that doesn't require custom nodes to test. If the bug open happens when certain custom nodes are used, most likely that custom node is what has the bug rather than ComfyUI, in which case it should be reported to the node's author."
description: "Describe how to reproduce the issue. Please be sure to attach a workflow JSON or PNG, ideally one that doesn't require custom nodes to test. If the bug open happens when certain custom nodes are used, most likely that custom node is what has the bug rather than Hanzo Studio, in which case it should be reported to the node's author."
validations:
required: true
- type: textarea
+6 -9
View File
@@ -1,11 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: ComfyUI Frontend Issues
url: https://github.com/Comfy-Org/ComfyUI_frontend/issues
about: Issues related to the ComfyUI frontend (display issues, user interaction bugs), please go to the frontend repo to file the issue
- name: ComfyUI Matrix Space
url: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
about: The ComfyUI Matrix Space is available for support and general discussion related to ComfyUI (Matrix is like Discord but open source).
- name: Comfy Org Discord
url: https://discord.gg/comfyorg
about: The Comfy Org Discord is available for support and general discussion related to ComfyUI.
- name: Hanzo Studio Frontend Issues
url: https://github.com/hanzoai/studio/issues
about: Issues related to the Hanzo Studio frontend (display issues, user interaction bugs).
- name: Hanzo AI Discord
url: https://discord.gg/hanzoai
about: The Hanzo AI Discord is available for support and general discussion related to Hanzo Studio.
+4 -4
View File
@@ -1,5 +1,5 @@
name: Feature Request
description: "You have an idea for something new you would like to see added to ComfyUI's core."
description: "You have an idea for something new you would like to see added to Hanzo Studio's core."
labels: [ "Feature" ]
body:
- type: markdown
@@ -7,11 +7,11 @@ body:
value: |
Before submitting a **Feature Request**, please ensure the following:
**1:** You are running the latest version of ComfyUI.
**1:** You are running the latest version of Hanzo Studio.
**2:** You have looked to make sure there is not already a feature that does what you need, and there is not already a Feature Request listed for the same idea.
**3:** This is something that makes sense to add to ComfyUI Core, and wouldn't make more sense as a custom node.
**3:** This is something that makes sense to add to Hanzo Studio Core, and wouldn't make more sense as a custom node.
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
If unsure, ask on the [Hanzo AI Discord](https://discord.gg/hanzoai) first.
- type: textarea
attributes:
label: Feature Idea
+3 -3
View File
@@ -7,17 +7,17 @@ body:
value: |
Before submitting a **User Report** issue, please ensure the following:
**1:** You are running the latest version of ComfyUI.
**1:** You are running the latest version of Hanzo Studio.
**2:** You have made an effort to find public answers to your question before asking here. In other words, you googled it first, and scrolled through recent help topics.
If unsure, ask on the [ComfyUI Matrix Space](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) or the [Comfy Org Discord](https://discord.gg/comfyorg) first.
If unsure, ask on the [Hanzo AI Discord](https://discord.gg/hanzoai) first.
- type: checkboxes
id: custom-nodes-test
attributes:
label: Custom Node Testing
description: Please confirm you have tried to reproduce the issue with all custom nodes disabled.
options:
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
- label: I have tried disabling custom nodes and the issue persists (see [how to disable custom nodes](https://docs.hanzo.ai/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled) if you need help)
required: false
- type: textarea
attributes:
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="studio">
<rect width="1280" height="640" fill="#0A0A0A"/>
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">studio</text>
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Hanzo Studio — Visual AI Engine</text>
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2 -2
View File
@@ -4,7 +4,7 @@ on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- 'comfy_api_nodes/**' # only run if these files changed
- 'studio_api_nodes/**' # only run if these files changed
permissions:
contents: read
@@ -12,7 +12,7 @@ permissions:
jobs:
inject:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Ensure template exists and append to PR body
uses: actions/github-script@v7
+1 -1
View File
@@ -6,7 +6,7 @@ on:
jobs:
check-line-endings:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout code
+22
View File
@@ -0,0 +1,22 @@
# Canonical CI/CD — imports the shared hanzoai/ci reusable workflow, which
# reads hanzo.yml (images + test + deploy + kms). No per-repo build logic,
# no GitHub-hosted runners (defaults to the Hanzo cloud arc pool).
name: CI/CD
on:
push:
branches: [main]
tags: ['v*']
pull_request:
workflow_dispatch:
jobs:
cicd:
uses: hanzoai/ci/.github/workflows/build.yml@v1
# Target the Hanzo arc scale set by its installation name. ARC scale sets
# are matched by name, not by the [self-hosted,linux,amd64] label triple
# that ci@v1 defaults to — without this override the job never gets a
# runner and sits queued.
with:
runner: '["hanzo-build-linux-amd64"]'
secrets: inherit
+5 -5
View File
@@ -1,5 +1,5 @@
# This is the GitHub Workflow that drives full-GPU-enabled tests of pull requests to ComfyUI, when the 'Run-CI-Test' label is added
# Results are reported as checkmarks on the commits, as well as onto https://ci.comfy.org/
# This is the GitHub Workflow that drives full-GPU-enabled tests of pull requests to Hanzo Studio, when the 'Run-CI-Test' label is added
# Results are reported as checkmarks on the commits, as well as onto https://ci.hanzo.ai/
name: Pull Request CI Workflow Runs
on:
pull_request_target:
@@ -34,11 +34,11 @@ jobs:
python_version: ${{ matrix.python_version }}
torch_version: ${{ matrix.torch_version }}
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
comfyui_flags: ${{ matrix.flags }}
studio_flags: ${{ matrix.flags }}
use_prior_commit: 'true'
comment:
if: ${{ github.event.label.name == 'Run-CI-Test' }}
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
permissions:
pull-requests: write
steps:
@@ -49,5 +49,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '(Automated Bot Message) CI Tests are running, you can view the results at https://ci.comfy.org/?branch=${{ github.event.pull_request.number }}%2Fmerge'
body: '(Automated Bot Message) CI Tests are running, you can view the results at https://ci.hanzo.ai/?branch=${{ github.event.pull_request.number }}%2Fmerge'
})
+4 -4
View File
@@ -6,7 +6,7 @@ on:
jobs:
send-webhook:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
env:
DESKTOP_REPO_DISPATCH_TOKEN: ${{ secrets.DESKTOP_REPO_DISPATCH_TOKEN }}
steps:
@@ -126,7 +126,7 @@ jobs:
--arg release_tag "$RELEASE_TAG" \
--arg release_url "$RELEASE_URL" \
'{
event_type: "comfyui_release_published",
event_type: "studio_release_published",
client_payload: {
release_tag: $release_tag,
release_url: $release_url
@@ -138,7 +138,7 @@ jobs:
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DISPATCH_TOKEN}" \
https://api.github.com/repos/Comfy-Org/desktop/dispatches \
https://api.github.com/repos/hanzoai/studio-desktop/dispatches \
-d "$PAYLOAD"
echo "✅ Dispatched ComfyUI release ${RELEASE_TAG} to Comfy-Org/desktop"
echo "✅ Dispatched Hanzo Studio release ${RELEASE_TAG} to hanzoai/studio-desktop"
+13 -8
View File
@@ -5,7 +5,7 @@ on: [push, pull_request]
jobs:
ruff:
name: Run Ruff
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout repository
@@ -16,15 +16,18 @@ jobs:
with:
python-version: 3.x
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install Ruff
run: pip install ruff
run: uv pip install --system ruff
- name: Run Ruff
run: ruff check .
pylint:
name: Run Pylint
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout repository
@@ -35,14 +38,16 @@ jobs:
with:
python-version: '3.12'
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
uv pip install --system torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
uv pip install --system -r requirements.txt
- name: Install Pylint
run: pip install pylint
run: uv pip install --system pylint
- name: Run Pylint
run: pylint comfy_api_nodes
run: pylint studio_api_nodes
+23 -23
View File
@@ -76,7 +76,7 @@ on:
default: true
jobs:
package_comfy_windows:
package_studio_windows:
permissions:
contents: "write"
packages: "write"
@@ -93,12 +93,12 @@ jobs:
with:
path: |
${{ inputs.cache_tag }}_python_deps.tar
update_comfyui_and_python_dependencies.bat
update_studio_and_python_dependencies.bat
key: ${{ runner.os }}-build-${{ inputs.cache_tag }}-${{ inputs.python_minor }}
- shell: bash
run: |
mv ${{ inputs.cache_tag }}_python_deps.tar ../
mv update_comfyui_and_python_dependencies.bat ../
mv update_studio_and_python_dependencies.bat ../
cd ..
tar xf ${{ inputs.cache_tag }}_python_deps.tar
pwd
@@ -107,7 +107,7 @@ jobs:
- shell: bash
run: |
cd ..
cp -r ComfyUI ComfyUI_copy
cp -r HanzoStudio HanzoStudio_copy
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
unzip python_embeded.zip -d python_embeded
cd python_embeded
@@ -117,11 +117,11 @@ jobs:
./python.exe get-pip.py
./python.exe -s -m pip install ../${{ inputs.cache_tag }}_python_deps/*
grep comfy ../ComfyUI/requirements.txt > ./requirements_comfyui.txt
./python.exe -s -m pip install -r requirements_comfyui.txt
rm requirements_comfyui.txt
grep -E "comfyui-frontend-package|comfyui-workflow-templates|comfyui-embedded-docs|comfy-kitchen|comfy-aimdo" ../HanzoStudio/requirements.txt > ./requirements_studio.txt
./python.exe -s -m pip install -r requirements_studio.txt
rm requirements_studio.txt
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
sed -i '1i../HanzoStudio' ./python3${{ inputs.python_minor }}._pth
if test -f ./Lib/site-packages/torch/lib/dnnl.lib; then
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
@@ -131,40 +131,40 @@ jobs:
cd ..
git clone --depth 1 https://github.com/comfyanonymous/taesd
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
git clone --depth 1 https://github.com/hanzoai/taesd
cp taesd/*.safetensors ./HanzoStudio_copy/models/vae_approx/
mkdir ComfyUI_windows_portable
mv python_embeded ComfyUI_windows_portable
mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI
mkdir HanzoStudio_windows_portable
mv python_embeded HanzoStudio_windows_portable
mv HanzoStudio_copy HanzoStudio_windows_portable/HanzoStudio
cd ComfyUI_windows_portable
cd HanzoStudio_windows_portable
mkdir update
cp -r ComfyUI/.ci/update_windows/* ./update/
cp -r ComfyUI/.ci/windows_${{ inputs.rel_name }}_base_files/* ./
cp ../update_comfyui_and_python_dependencies.bat ./update/
cp -r HanzoStudio/.ci/update_windows/* ./update/
cp -r HanzoStudio/.ci/windows_${{ inputs.rel_name }}_base_files/* ./
cp ../update_studio_and_python_dependencies.bat ./update/
cd ..
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable
mv ComfyUI_windows_portable.7z ComfyUI/ComfyUI_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 HanzoStudio_windows_portable.7z HanzoStudio_windows_portable
mv HanzoStudio_windows_portable.7z HanzoStudio/HanzoStudio_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
- shell: bash
if: ${{ inputs.test_release }}
run: |
cd ..
cd ComfyUI_windows_portable
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
cd HanzoStudio_windows_portable
python_embeded/python.exe -s HanzoStudio/main.py --quick-test-for-ci --cpu
python_embeded/python.exe -s ./update/update.py ComfyUI/
python_embeded/python.exe -s ./update/update.py HanzoStudio/
ls
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ComfyUI_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
files: HanzoStudio_windows_portable_${{ inputs.rel_name }}${{ inputs.rel_extra_name }}.7z
tag_name: ${{ inputs.git_tag }}
draft: true
overwrite_files: true
+1 -1
View File
@@ -8,7 +8,7 @@ permissions:
jobs:
stale:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/stale@v9
with:
+4 -3
View File
@@ -14,7 +14,7 @@ on:
jobs:
build:
name: Build Test
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
strategy:
fail-fast: false
matrix:
@@ -25,7 +25,8 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
uv pip install --system -r requirements.txt
+6 -6
View File
@@ -1,6 +1,6 @@
# This is the GitHub Workflow that drives automatic full-GPU-enabled tests of all new commits to the master branch of ComfyUI
# Results are reported as checkmarks on the commits, as well as onto https://ci.comfy.org/
name: Full Comfy CI Workflow Runs
# This is the GitHub Workflow that drives automatic full-GPU-enabled tests of all new commits to the main branch of Hanzo Studio
# Results are reported as checkmarks on the commits, as well as onto https://ci.hanzo.ai/
name: Full Hanzo Studio CI Workflow Runs
on:
push:
branches:
@@ -46,7 +46,7 @@ jobs:
python_version: ${{ matrix.python_version }}
torch_version: ${{ matrix.torch_version }}
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
comfyui_flags: ${{ matrix.flags }}
studio_flags: ${{ matrix.flags }}
# test-win-nightly:
# strategy:
@@ -69,7 +69,7 @@ jobs:
# python_version: ${{ matrix.python_version }}
# torch_version: ${{ matrix.torch_version }}
# google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
# comfyui_flags: ${{ matrix.flags }}
# studio_flags: ${{ matrix.flags }}
test-unix-nightly:
strategy:
@@ -96,4 +96,4 @@ jobs:
python_version: ${{ matrix.python_version }}
torch_version: ${{ matrix.torch_version }}
google_credentials: ${{ secrets.GCS_SERVICE_ACCOUNT_JSON }}
comfyui_flags: ${{ matrix.flags }}
studio_flags: ${{ matrix.flags }}
+5 -4
View File
@@ -19,12 +19,13 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install -r tests-unit/requirements.txt
uv pip install --system torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
uv pip install --system -r requirements.txt
uv pip install --system -r tests-unit/requirements.txt
- name: Run Execution Tests
run: |
python -m pytest tests/execution -v --skip-timing-checks
+15 -14
View File
@@ -8,28 +8,29 @@ on:
jobs:
test:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout ComfyUI
- name: Checkout Hanzo Studio
uses: actions/checkout@v4
with:
repository: "Comfy-Org/ComfyUI"
path: "ComfyUI"
repository: "hanzoai/studio"
path: "studio"
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Start ComfyUI server
uv pip install --system torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
uv pip install --system -r requirements.txt
uv pip install --system wait-for-it
working-directory: studio
- name: Start Hanzo Studio server
run: |
python main.py --cpu 2>&1 | tee console_output.log &
wait-for-it --service 127.0.0.1:8188 -t 30
working-directory: ComfyUI
wait-for-it --service 127.0.0.1:8188 -t 120
working-directory: studio
- name: Check for unhandled exceptions in server log
run: |
grep -v "Found comfy_kitchen backend triton: {'available': False, 'disabled': True, 'unavailable_reason': \"ImportError: No module named 'triton'\", 'capabilities': \[\]}" console_output.log | grep -v "Found comfy_kitchen backend triton: {'available': False, 'disabled': False, 'unavailable_reason': \"ImportError: No module named 'triton'\", 'capabilities': \[\]}" > console_output_filtered.log
@@ -38,10 +39,10 @@ jobs:
echo "Unhandled exception/error found in server log."
exit 1
fi
working-directory: ComfyUI
working-directory: studio
- uses: actions/upload-artifact@v4
if: always()
with:
name: console-output
path: ComfyUI/console_output.log
path: studio/console_output.log
retention-days: 30
+19 -19
View File
@@ -1,4 +1,4 @@
name: Generate Pydantic Stubs from api.comfy.org
name: Generate Pydantic Stubs from api.hanzo.ai
on:
schedule:
@@ -7,50 +7,50 @@ on:
jobs:
generate-models:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install 'datamodel-code-generator[http]'
npm install @redocly/cli
- name: Download OpenAPI spec
run: |
curl -o openapi.yaml https://api.comfy.org/openapi
curl -o openapi.yaml https://api.hanzo.ai/openapi
- name: Filter OpenAPI spec with Redocly
run: |
npx @redocly/cli bundle openapi.yaml --output filtered-openapi.yaml --config comfy_api_nodes/redocly.yaml --remove-unused-components
npx @redocly/cli bundle openapi.yaml --output filtered-openapi.yaml --config studio_api_nodes/redocly.yaml --remove-unused-components
- name: Generate API models
run: |
datamodel-codegen --use-subclass-enum --input filtered-openapi.yaml --output comfy_api_nodes/apis --output-model-type pydantic_v2.BaseModel
datamodel-codegen --use-subclass-enum --input filtered-openapi.yaml --output studio_api_nodes/apis --output-model-type pydantic_v2.BaseModel
- name: Check for changes
id: git-check
run: |
git diff --exit-code comfy_api_nodes/apis || echo "changes=true" >> $GITHUB_OUTPUT
git diff --exit-code studio_api_nodes/apis || echo "changes=true" >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.git-check.outputs.changes == 'true'
uses: peter-evans/create-pull-request@v5
with:
commit-message: 'chore: update API models from OpenAPI spec'
title: 'Update API models from api.comfy.org'
title: 'Update API models from api.hanzo.ai'
body: |
This PR updates the API models based on the latest api.comfy.org OpenAPI specification.
Generated automatically by the a Github workflow.
This PR updates the API models based on the latest api.hanzo.ai OpenAPI specification.
Generated automatically by a Github workflow.
branch: update-api-stubs
delete-branch: true
base: master
base: main
+10 -10
View File
@@ -6,13 +6,13 @@ on:
workflow_dispatch:
inputs:
version:
description: 'ComfyUI version (e.g., v0.7.0)'
description: 'Hanzo Studio version (e.g., v0.7.0)'
required: true
type: string
jobs:
update-ci-container:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
# Skip pre-releases unless manually triggered
if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
steps:
@@ -26,34 +26,34 @@ jobs:
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Checkout comfyui-ci-container
- name: Checkout studio-ci-container
uses: actions/checkout@v4
with:
repository: comfy-org/comfyui-ci-container
repository: hanzoai/studio-ci-container
token: ${{ secrets.CI_CONTAINER_PAT }}
- name: Check current version
id: current
run: |
CURRENT=$(grep -oP 'ARG COMFYUI_VERSION=\K.*' Dockerfile || echo "unknown")
CURRENT=$(grep -oP 'ARG STUDIO_VERSION=\K.*' Dockerfile || echo "unknown")
echo "current_version=$CURRENT" >> $GITHUB_OUTPUT
- name: Update Dockerfile
run: |
VERSION="${{ steps.version.outputs.version }}"
sed -i "s/^ARG COMFYUI_VERSION=.*/ARG COMFYUI_VERSION=${VERSION}/" Dockerfile
sed -i "s/^ARG STUDIO_VERSION=.*/ARG STUDIO_VERSION=${VERSION}/" Dockerfile
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.CI_CONTAINER_PAT }}
branch: automation/comfyui-${{ steps.version.outputs.version }}
title: "chore: bump ComfyUI to ${{ steps.version.outputs.version }}"
branch: automation/studio-${{ steps.version.outputs.version }}
title: "chore: bump Hanzo Studio to ${{ steps.version.outputs.version }}"
body: |
Updates ComfyUI version from `${{ steps.current.outputs.current_version }}` to `${{ steps.version.outputs.version }}`
Updates Hanzo Studio version from `${{ steps.current.outputs.current_version }}` to `${{ steps.version.outputs.version }}`
**Triggered by:** ${{ github.event_name == 'release' && format('[Release {0}]({1})', github.event.release.tag_name, github.event.release.html_url) || 'Manual workflow dispatch' }}
labels: automation
commit-message: "chore: bump ComfyUI to ${{ steps.version.outputs.version }}"
commit-message: "chore: bump Hanzo Studio to ${{ steps.version.outputs.version }}"
+7 -7
View File
@@ -10,7 +10,7 @@ on:
jobs:
update-version:
runs-on: ubuntu-latest
runs-on: hanzo-build-linux-amd64
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
@@ -30,9 +30,9 @@ jobs:
run: |
python -m pip install --upgrade pip
- name: Update comfyui_version.py
- name: Update studio_version.py
run: |
# Read version from pyproject.toml and update comfyui_version.py
# Read version from pyproject.toml and update studio_version.py
python -c '
import tomllib
@@ -41,8 +41,8 @@ jobs:
config = tomllib.load(f)
version = config["project"]["version"]
# Write version to comfyui_version.py
with open("comfyui_version.py", "w") as f:
# Write version to studio_version.py
with open("studio_version.py", "w") as f:
f.write("# This file is automatically generated by the build process when version is\n")
f.write("# updated in pyproject.toml.\n")
f.write(f"__version__ = \"{version}\"\n")
@@ -54,6 +54,6 @@ jobs:
git config --local user.email "github-actions@github.com"
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add comfyui_version.py
git diff --quiet && git diff --staged --quiet || git commit -m "chore: Update comfyui_version.py to match pyproject.toml"
git add studio_version.py
git diff --quiet && git diff --staged --quiet || git commit -m "chore: Update studio_version.py to match pyproject.toml"
git push origin HEAD:${{ github.head_ref }}
@@ -46,18 +46,18 @@ jobs:
- shell: bash
run: |
echo "@echo off
call update_comfyui.bat nopause
call update_studio.bat nopause
echo -
echo This will try to update pytorch and all python dependencies.
echo -
echo If you just want to update normally, close this and run update_comfyui.bat instead.
echo If you just want to update normally, close this and run update_studio.bat instead.
echo -
pause
..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
pause" > update_comfyui_and_python_dependencies.bat
..\python_embeded\python.exe -s -m pip install --upgrade torch torchvision torchaudio ${{ inputs.xformers }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r ../HanzoStudio/requirements.txt pygit2
pause" > update_studio_and_python_dependencies.bat
grep -v comfyui requirements.txt > requirements_nocomfyui.txt
python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements_nocomfyui.txt pygit2 -w ./temp_wheel_dir
grep -v studio requirements.txt > requirements_nostudio.txt
python -m pip wheel --no-cache-dir torch torchvision torchaudio ${{ inputs.xformers }} ${{ inputs.extra_dependencies }} --extra-index-url https://download.pytorch.org/whl/cu${{ inputs.cu }} -r requirements_nostudio.txt pygit2 -w ./temp_wheel_dir
python -m pip install --no-cache-dir ./temp_wheel_dir/*
echo installed basic
ls -lah temp_wheel_dir
@@ -68,5 +68,5 @@ jobs:
with:
path: |
cu${{ inputs.cu }}_python_deps.tar
update_comfyui_and_python_dependencies.bat
update_studio_and_python_dependencies.bat
key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }}
@@ -38,18 +38,18 @@ jobs:
- shell: bash
run: |
echo "@echo off
call update_comfyui.bat nopause
call update_studio.bat nopause
echo -
echo This will try to update pytorch and all python dependencies.
echo -
echo If you just want to update normally, close this and run update_comfyui.bat instead.
echo If you just want to update normally, close this and run update_studio.bat instead.
echo -
pause
..\python_embeded\python.exe -s -m pip install --upgrade ${{ inputs.torch_dependencies }} -r ../ComfyUI/requirements.txt pygit2
pause" > update_comfyui_and_python_dependencies.bat
..\python_embeded\python.exe -s -m pip install --upgrade ${{ inputs.torch_dependencies }} -r ../HanzoStudio/requirements.txt pygit2
pause" > update_studio_and_python_dependencies.bat
grep -v comfyui requirements.txt > requirements_nocomfyui.txt
python -m pip wheel --no-cache-dir ${{ inputs.torch_dependencies }} -r requirements_nocomfyui.txt pygit2 -w ./temp_wheel_dir
grep -v studio requirements.txt > requirements_nostudio.txt
python -m pip wheel --no-cache-dir ${{ inputs.torch_dependencies }} -r requirements_nostudio.txt pygit2 -w ./temp_wheel_dir
python -m pip install --no-cache-dir ./temp_wheel_dir/*
echo installed basic
ls -lah temp_wheel_dir
@@ -60,5 +60,5 @@ jobs:
with:
path: |
${{ inputs.cache_tag }}_python_deps.tar
update_comfyui_and_python_dependencies.bat
update_studio_and_python_dependencies.bat
key: ${{ runner.os }}-build-${{ inputs.cache_tag }}-${{ inputs.python_minor }}
@@ -42,45 +42,45 @@ jobs:
- shell: bash
run: |
cd ..
cp -r ComfyUI ComfyUI_copy
cp -r HanzoStudio HanzoStudio_copy
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
unzip python_embeded.zip -d python_embeded
cd python_embeded
echo 'import site' >> ./python3${{ inputs.python_minor }}._pth
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
./python.exe get-pip.py
python -m pip wheel torch torchvision torchaudio --pre --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2 -w ../temp_wheel_dir
python -m pip wheel torch torchvision torchaudio --pre --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../HanzoStudio/requirements.txt pygit2 -w ../temp_wheel_dir
ls ../temp_wheel_dir
./python.exe -s -m pip install --pre ../temp_wheel_dir/*
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
sed -i '1i../HanzoStudio' ./python3${{ inputs.python_minor }}._pth
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
cd ..
git clone --depth 1 https://github.com/comfyanonymous/taesd
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
git clone --depth 1 https://github.com/hanzoai/taesd
cp taesd/*.safetensors ./HanzoStudio_copy/models/vae_approx/
mkdir ComfyUI_windows_portable_nightly_pytorch
mv python_embeded ComfyUI_windows_portable_nightly_pytorch
mv ComfyUI_copy ComfyUI_windows_portable_nightly_pytorch/ComfyUI
mkdir HanzoStudio_windows_portable_nightly_pytorch
mv python_embeded HanzoStudio_windows_portable_nightly_pytorch
mv HanzoStudio_copy HanzoStudio_windows_portable_nightly_pytorch/HanzoStudio
cd ComfyUI_windows_portable_nightly_pytorch
cd HanzoStudio_windows_portable_nightly_pytorch
mkdir update
cp -r ComfyUI/.ci/update_windows/* ./update/
cp -r ComfyUI/.ci/windows_nvidia_base_files/* ./
cp -r ComfyUI/.ci/windows_nightly_base_files/* ./
cp -r HanzoStudio/.ci/update_windows/* ./update/
cp -r HanzoStudio/.ci/windows_nvidia_base_files/* ./
cp -r HanzoStudio/.ci/windows_nightly_base_files/* ./
echo "call update_comfyui.bat nopause
..\python_embeded\python.exe -s -m pip install --upgrade --pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../ComfyUI/requirements.txt pygit2
pause" > ./update/update_comfyui_and_python_dependencies.bat
echo "call update_studio.bat nopause
..\python_embeded\python.exe -s -m pip install --upgrade --pre torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/nightly/cu${{ inputs.cu }} -r ../HanzoStudio/requirements.txt pygit2
pause" > ./update/update_studio_and_python_dependencies.bat
cd ..
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=512m -ms=on -mf=BCJ2 ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI_windows_portable_nightly_pytorch
mv ComfyUI_windows_portable_nightly_pytorch.7z ComfyUI/ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=512m -ms=on -mf=BCJ2 HanzoStudio_windows_portable_nightly_pytorch.7z HanzoStudio_windows_portable_nightly_pytorch
mv HanzoStudio_windows_portable_nightly_pytorch.7z HanzoStudio/HanzoStudio_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
cd ComfyUI_windows_portable_nightly_pytorch
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
cd HanzoStudio_windows_portable_nightly_pytorch
python_embeded/python.exe -s HanzoStudio/main.py --quick-test-for-ci --cpu
ls
@@ -88,6 +88,6 @@ jobs:
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ComfyUI_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
file: HanzoStudio_windows_portable_nvidia_or_cpu_nightly_pytorch.7z
tag: "latest"
overwrite: true
+20 -20
View File
@@ -25,7 +25,7 @@ on:
# - master
jobs:
package_comfyui:
package_studio:
permissions:
contents: "write"
packages: "write"
@@ -37,12 +37,12 @@ jobs:
with:
path: |
cu${{ inputs.cu }}_python_deps.tar
update_comfyui_and_python_dependencies.bat
update_studio_and_python_dependencies.bat
key: ${{ runner.os }}-build-cu${{ inputs.cu }}-${{ inputs.python_minor }}
- shell: bash
run: |
mv cu${{ inputs.cu }}_python_deps.tar ../
mv update_comfyui_and_python_dependencies.bat ../
mv update_studio_and_python_dependencies.bat ../
cd ..
tar xf cu${{ inputs.cu }}_python_deps.tar
pwd
@@ -55,7 +55,7 @@ jobs:
- shell: bash
run: |
cd ..
cp -r ComfyUI ComfyUI_copy
cp -r HanzoStudio HanzoStudio_copy
curl https://www.python.org/ftp/python/3.${{ inputs.python_minor }}.${{ inputs.python_patch }}/python-3.${{ inputs.python_minor }}.${{ inputs.python_patch }}-embed-amd64.zip -o python_embeded.zip
unzip python_embeded.zip -d python_embeded
cd python_embeded
@@ -63,36 +63,36 @@ jobs:
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
./python.exe get-pip.py
./python.exe -s -m pip install ../cu${{ inputs.cu }}_python_deps/*
sed -i '1i../ComfyUI' ./python3${{ inputs.python_minor }}._pth
sed -i '1i../HanzoStudio' ./python3${{ inputs.python_minor }}._pth
rm ./Lib/site-packages/torch/lib/dnnl.lib #I don't think this is actually used and I need the space
rm ./Lib/site-packages/torch/lib/libprotoc.lib
rm ./Lib/site-packages/torch/lib/libprotobuf.lib
cd ..
git clone --depth 1 https://github.com/comfyanonymous/taesd
cp taesd/*.safetensors ./ComfyUI_copy/models/vae_approx/
git clone --depth 1 https://github.com/hanzoai/taesd
cp taesd/*.safetensors ./HanzoStudio_copy/models/vae_approx/
mkdir ComfyUI_windows_portable
mv python_embeded ComfyUI_windows_portable
mv ComfyUI_copy ComfyUI_windows_portable/ComfyUI
mkdir HanzoStudio_windows_portable
mv python_embeded HanzoStudio_windows_portable
mv HanzoStudio_copy HanzoStudio_windows_portable/HanzoStudio
cd ComfyUI_windows_portable
cd HanzoStudio_windows_portable
mkdir update
cp -r ComfyUI/.ci/update_windows/* ./update/
cp -r ComfyUI/.ci/windows_nvidia_base_files/* ./
cp ../update_comfyui_and_python_dependencies.bat ./update/
cp -r HanzoStudio/.ci/update_windows/* ./update/
cp -r HanzoStudio/.ci/windows_nvidia_base_files/* ./
cp ../update_studio_and_python_dependencies.bat ./update/
cd ..
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 ComfyUI_windows_portable.7z ComfyUI_windows_portable
mv ComfyUI_windows_portable.7z ComfyUI/new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
"C:\Program Files\7-Zip\7z.exe" a -t7z -m0=lzma2 -mx=9 -mfb=128 -md=768m -ms=on -mf=BCJ2 HanzoStudio_windows_portable.7z HanzoStudio_windows_portable
mv HanzoStudio_windows_portable.7z HanzoStudio/new_HanzoStudio_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
cd ComfyUI_windows_portable
python_embeded/python.exe -s ComfyUI/main.py --quick-test-for-ci --cpu
cd HanzoStudio_windows_portable
python_embeded/python.exe -s HanzoStudio/main.py --quick-test-for-ci --cpu
python_embeded/python.exe -s ./update/update.py ComfyUI/
python_embeded/python.exe -s ./update/update.py HanzoStudio/
ls
@@ -100,7 +100,7 @@ jobs:
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: new_ComfyUI_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
file: new_HanzoStudio_windows_portable_nvidia_cu${{ inputs.cu }}_or_cpu.7z
tag: "latest"
overwrite: true
+7
View File
@@ -0,0 +1,7 @@
name: Workflow Sanity
on:
pull_request:
paths: ['.github/workflows/**']
jobs:
sanity:
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
+8 -3
View File
@@ -5,13 +5,17 @@ __pycache__/
!/input/example.png
/models/
/temp/
/custom_nodes/
!custom_nodes/example_node.py.example
# Ignore vendored pack contents but keep the directory listable so the tracked
# files below can be re-included (a file under a fully-excluded dir cannot be).
/custom_nodes/*
!/custom_nodes/example_node.py.example
# Pinned pack manifest is tracked; the pack sources it clones are not (vendored at build).
!/custom_nodes/hanzo-packs.txt
extra_model_paths.yaml
/.vs
.vscode/
.idea/
venv/
venv*/
.venv/
/web/extensions/*
!/web/extensions/logging.js.example
@@ -24,3 +28,4 @@ web_custom_versions/
openapi.yaml
filtered-openapi.yaml
uv.lock
/custom_nodes_quarantine/
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
Symlink
+1
View File
@@ -0,0 +1 @@
LLM.md
+1 -1
View File
@@ -1,2 +1,2 @@
# Admins
* @comfyanonymous @kosinkadink @guill
* @hanzoai/studio
+8 -8
View File
@@ -1,12 +1,12 @@
# Contributing to ComfyUI
# Contributing to Hanzo Studio
Welcome, and thank you for your interest in contributing to ComfyUI!
Welcome, and thank you for your interest in contributing to Hanzo Studio!
There are several ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved.
## Asking Questions
Have a question? Instead of opening an issue, please ask on [Discord](https://comfy.org/discord) or [Matrix](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) channels. Our team and the community will help you.
Have a question? Instead of opening an issue, please ask on [Discord](https://discord.gg/hanzoai) first. Our team and the community will help you.
## Providing Feedback
@@ -16,24 +16,24 @@ See the `#bug-report`, `#feature-request` and `#feedback` channels on Discord.
## Reporting Issues
Have you identified a reproducible problem in ComfyUI? Do you have a feature request? We want to hear about it! Here's how you can report your issue as effectively as possible.
Have you identified a reproducible problem in Hanzo Studio? Do you have a feature request? We want to hear about it! Here's how you can report your issue as effectively as possible.
### Look For an Existing Issue
Before you create a new issue, please do a search in [open issues](https://github.com/comfyanonymous/ComfyUI/issues) to see if the issue or feature request has already been filed.
Before you create a new issue, please do a search in [open issues](https://github.com/hanzoai/studio/issues) to see if the issue or feature request has already been filed.
If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment:
* 👍 - upvote
* 👎 - downvote
* upvote
* downvote
If you cannot find an existing issue that describes your bug or feature, create a new issue. We have an issue template in place to organize new issues.
### Creating Pull Requests
* Please refer to the article on [creating pull requests](https://github.com/comfyanonymous/ComfyUI/wiki/How-to-Contribute-Code) and contributing to this project.
* Please refer to the article on [creating pull requests](https://github.com/hanzoai/studio/wiki/How-to-Contribute-Code) and contributing to this project.
## Thank You
+80
View File
@@ -0,0 +1,80 @@
FROM python:3.11-slim AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
build-essential \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch CPU first (large layer, cached separately)
RUN pip install --no-cache-dir \
torch torchvision torchaudio \
--index-url https://download.pytorch.org/whl/cpu
# Install remaining dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Apply Hanzo Studio branding to frontend package
COPY branding/ /tmp/branding/
RUN chmod +x /tmp/branding/apply-branding.sh && /tmp/branding/apply-branding.sh && rm -rf /tmp/branding
# Copy application code
COPY . .
# Vendor the Hanzo Studio custom-node packs (segmentation, controlnet, ipadapter,
# upscaling, video, utils) at their pinned commits and install their deps with the
# torch/numpy/transformers stack locked. See custom_nodes/hanzo-packs.txt.
RUN chmod +x scripts/install_custom_nodes.sh && scripts/install_custom_nodes.sh
# Create directories for runtime volumes
RUN mkdir -p models output input custom_nodes user
# /app/orgs is the tenant-data PVC; /app/models is ephemeral overlay. Register
# .models on the PVC as the default model store (main.py auto-loads this file)
# so models survive pod restarts and downloads land durable.
RUN printf '%s\n' \
'hanzo:' \
' base_path: /app/orgs/.models' \
' is_default: true' \
' checkpoints: checkpoints' \
' configs: configs' \
' loras: loras' \
' vae: vae' \
' text_encoders: text_encoders' \
' diffusion_models: diffusion_models' \
' clip_vision: clip_vision' \
' style_models: style_models' \
' embeddings: embeddings' \
' diffusers: diffusers' \
' vae_approx: vae_approx' \
' controlnet: controlnet' \
' gligen: gligen' \
' upscale_models: upscale_models' \
' latent_upscale_models: latent_upscale_models' \
' hypernetworks: hypernetworks' \
' photomaker: photomaker' \
' classifiers: classifiers' \
' model_patches: model_patches' \
' audio_encoders: audio_encoders' \
> extra_model_paths.yaml
EXPOSE 8188
# Default: listen on all interfaces, CPU mode
# Environment variables:
# STUDIO_ENABLE_IAM_AUTH=1 - Enable IAM auth via hanzo.id
# STUDIO_IAM_URL=https://hanzo.id
# STUDIO_NO_LOCALHOST_BYPASS=1 - Require auth even for localhost
# STUDIO_MULTI_TENANT=1 - Per-org storage isolation
# STUDIO_ORG_ID=default-org - Default org ID
# STUDIO_ENABLE_BILLING=1 - Commerce billing integration
# STUDIO_BILLING_CHECK_BALANCE=1 - Check balance before prompts
# STUDIO_COMMERCE_URL=http://commerce.hanzo.svc:8001
# STUDIO_COMMERCE_TOKEN=... - Commerce API token
# STUDIO_ENABLE_METRICS=1 - /metrics endpoint for VictoriaMetrics
# STUDIO_RATE_LIMIT_RPM=60 - Prompts per minute per org
CMD ["python", "main.py", "--listen", "0.0.0.0", "--port", "8188", "--cpu"]
+228
View File
@@ -0,0 +1,228 @@
# Hanzo Studio
## Overview
**The most powerful and modular visual AI engine and application.**
## Tech Stack
- **Language**: Python
## Build & Run
```bash
uv sync
uv run pytest
```
## Structure
```
studio/
CODEOWNERS
CONTRIBUTING.md
Dockerfile
LICENSE
QUANTIZATION.md
README.md
alembic.ini
alembic_db/
api_server/
app/
blueprints/
branding/
comfyui.log
comfyui.prev.log
comfyui.prev2.log
```
## Key Files
- `README.md` -- Project documentation
- `pyproject.toml` -- Python project config
- `Dockerfile` -- Container build (applies `branding/` then runs `main.py`)
- `hanzo.yml` + `.github/workflows/cicd.yml` -- canonical CI/CD (hanzoai/ci); builds `ghcr.io/hanzoai/studio`, rolls the `studio` operator Service CR at `studio.hanzo.ai`.
## Custom-node packs ("hanzo packages")
The popular ComfyUI packs are vendored as first-class hanzo packages, pinned and
reproducible. One and only one way to add/bump them:
- **Manifest**: `custom_nodes/hanzo-packs.txt` — `<dir> <git_url> <commit_sha>
<spdx>` lines (immutable SHAs). This is the tracked source of truth; the cloned
pack sources are gitignored (vendored at build).
- **Deps**: `custom_nodes-requirements.txt` — hand-resolved, only what's not
already in `requirements.txt`. Installed with the studio torch(cu130)/NumPy 2.x/
Transformers 5.x stack **locked** so packs can never downgrade it.
- **Installer**: `scripts/install_custom_nodes.sh` clones each pack at its pin and
installs deps; the lock is derived from the live env (correct for both the CPU
image and the GB10 cu130 venv). `ultralytics` is installed `--no-deps` (else it
pulls non-headless opencv). Local (GB10): `STUDIO_PY=.venv/bin/python
STUDIO_PIP="uv pip install --python .venv/bin/python" scripts/install_custom_nodes.sh`.
- **Image**: `Dockerfile` runs the installer after `COPY . .`; `.dockerignore`
re-includes only the manifest + installer (`custom_nodes/*` / `scripts/*` form).
So `git push origin main` → CI builds `ghcr.io/hanzoai/studio` with the packs →
studio.hanzo.ai gets them on the next image build.
**Compat shim — why packs work here at all.** This fork renamed ComfyUI's core
packages (`comfy`→`studio`, `comfy_extras`→`studio_extras`, `comfy/comfy_types`→
`studio/node_types`, and the `Comfy*`→`Studio*` API classes; see
`scripts/rename_modules.py`). `studio_compat/` is one meta-path finder that lazily
aliases every `comfy*` import to its `studio*` counterpart and mirrors
`ComfyExtension`/`io.ComfyNode`→`Studio*` — so upstream packs run **unmodified**,
no forks/renames. It is the phase-4 redirect `rename_modules.py` deferred, done
with the 3.12 `find_spec` API. Activated by `import studio_compat` at the top of
`main.py`. `nodes.py` also honors the upstream `comfy_entrypoint` V3 name.
Restart the local worker only when idle: `scripts/studio-service-swap.sh` polls
`/queue` and restarts on the first empty window. Bad packs are isolated by the
loader (studio still boots); genuinely-incompatible ones go to
`custom_nodes_quarantine/` (e.g. was-node-suite: hard `numba` dep vs NumPy 2.5).
## Multi-tenant + IAM (cloud: studio.hanzo.ai)
Standalone app; `console.hanzo.ai` link-outs land here already authenticated via
shared Hanzo IAM (standard OIDC code flow). No embedding.
- **Auth** (`middleware/iam_auth_middleware.py`): local **JWKS** JWT validation
against `${STUDIO_IAM_URL}/v1/iam/.well-known/jwks` — signature + `exp` +
`iss` + `aud` (client_id `hanzo-studio`). Org from the `owner` claim
(`organization` fallback) → `request["iam_user"]["org_id"]`. Anonymous mode:
off by default locally (loopback bypass); enable in cloud with
`STUDIO_ENABLE_IAM_AUTH=true` (the one auth switch — this is the
"STUDIO_AUTH=off default" behavior). Browser flow: unauth HTML → IAM
`authorize` (code+PKCE) → `/callback` exchanges the code, sets an httpOnly
session cookie (no frontend changes). `verify_jwt(...)` is a pure, unit-tested
function.
- **Tenancy** (`folder_paths.py`, `app/user_manager.py`): with
`STUDIO_MULTI_TENANT=true`, userdata is `user/orgs/{org}/user/{user}/…` and
outputs are `output/orgs/{org}/…`. `get_public_user_directory(user, org)` is
org-rooted (fixed the bug where org userdata always resolved to `None`).
Execution outputs are scoped via `folder_paths.set_execution_org()`, bound by
the prompt worker from the queue item's `org_id`.
- **GPU federation** (`middleware/{worker_client,prompt_router,compute_config,
visor_client}.py`): `/v1/workers/register` (heartbeat), `/v1/workers` (list),
`/v1/worker/execute` (in-cluster push fast path). Worker↔coordinator trust via
`X-Worker-Token` (`STUDIO_WORKER_TOKEN`, KMS-sourced).
- **Per-GPU gpu-jobs queue** (`middleware/{gpu_dispatch,studio_home}.py`, `server.py`) —
the ONE submit path onto a GPU. A model-less coordinator's `POST /prompt` enqueues to
the cloud tasks engine (`POST {STUDIO_CLOUD_API_URL}/v1/tasks/namespaces/gpu-jobs/activities`,
user bearer → org-scoped); a BYO worker (`hanzo gpu connect`) claims + runs it.
**Targeting**: the Home "Run on <gpu>" chip sets `X-Target-GPU: <identity>`;
`dispatch_if_worker` enqueues on that machine's own lane `taskQueue="gpu:<identity>"`
(namespace stays `gpu-jobs`), else the shared `gpu-jobs` lane any worker drains —
`X-Target-GPU` rides through `_queue_prompt` so fix/compose/video/rerun all honor it.
**Authoritative visible queue**: `/v1/render-queue` + `/v1/nodes` read the tasks
engine's activity list — REAL claiming `identity`, status
SCHEDULED/STARTED/COMPLETED/FAILED → queued/running/done/failed, `lastHeartbeatTime`
liveness, per-node depth — not a local shadow; `render_jobs.json` survives only as a
~2s pre-claim placeholder. **No hidden runs**: in `--worker-mode` a direct `/prompt`
POST is refused 403 unless it carries `X-Worker-Token` — the only accepted execution is
a claimed job handed in over `POST /v1/worker/execute`
(`worker_client.reject_untrusted_worker_submit`, one policy gate). Legacy push
(`prompt_router`) is OFF unless `STUDIO_LEGACY_PUSH_ROUTER=1`. Tests:
`middleware_test/{gpu_dispatch_test,worker_seam_test}.py`.
- **Durable render queue** (`middleware/tasks_queue.py`, `docs/federation.md`):
`SqlitePromptQueue` — crash-durable render queue in a single SQLite file
(stdlib `sqlite3`, WAL, **zero external processes**). `STUDIO_QUEUE_DB=<path>`
→ `server.py` swaps `PromptQueue` for it (precedence **STUDIO_QUEUE_DB >
STUDIO_PERSIST_QUEUE > memory**; default local behavior unchanged). The
`PromptQueue` seam maps to a durable job state machine: `put→INSERT (pending,
idempotent on prompt_id)`, `get→claim next pending under BEGIN IMMEDIATE
(exactly-once, leased)`, `task_done→done | retry/failed`. Crash-recovery: a
lease + reap returns an abandoned claim to `pending` (on every get(), a
heartbeat thread, and on boot) so another process re-runs it — idempotent
(SaveImage suffixes increment). Multiple Studio processes on the same DB file
compete safely. Tested: `middleware_test/tasks_queue_test.py` (incl.
crash-recovery across instances). Future (federation.md §6): a remote queue
backend served by Hanzo Tasks + a Go/unified-binary (HIP-0106) migration.
- **Stacks** (`middleware/stacks_store.py`, `middleware/studio_home.py`): Procreate-
style gallery folders — a per-org SQLite store (`orgs/<org>/stacks.db`, WAL, every
mutation under `BEGIN IMMEDIATE`, the same `tasks_queue.py` pattern) that is ONLY an
organizational layer; assets stay the source of truth (library.json + files). Schema
= the spec's `stacks` + `stack_assets` join; `UNIQUE(asset_id)` ⇒ one Stack per
asset. Tenancy is structural (one DB file per org → cross-org 404s by construction).
REST (`/v1` house style): `GET/POST /v1/stacks`, `GET/PATCH/DELETE /v1/stacks/:id`
(`?mode=only|images`), `POST|DELETE /v1/stacks/:id/assets`, `PATCH /v1/stacks/:id/order`,
`POST /v1/stacks/:id/unstack`, `GET/PATCH /v1/stacks/settings`; registered via
injected resolvers (org/owner/workspace + the library soft-deleter) so the module
stays decoupled. Remove ≠ delete (returns to the gallery); Delete-Stack defaults to
preserving; "…and Images" maps to the library soft-delete (`status=deleted`, files
kept). Generation integration: `fix/compose/rerun/template` accept a `stack` id and
the ingest point (`_index_upload` → `stacks_store.absorb`) assigns membership when
outputs land (ALL outputs of a multi-output job); a per-org `auto_stack_batches`
setting auto-creates `"<prompt> — <date>"` Stacks. UI is the vanilla-JS home
(`studio_home.html`): layered cards, drag/multi-select/mobile-tap parity (no DnD ever
required), a11y labels + live region. Tested: `studio_home_test/test_stacks.py`
(routes, tenancy, one-per-asset, order persistence, delete/unstack, ingest→absorb).
- **Engine selector** (`middleware/engine_selector.py`): `/v1/engines` lists
execution targets for the org (`local` + registered compute_config workers) and
`PUT /v1/engines/default` sets a per-org default (stored on
`ComputeProfile.default_engine`). `prompt_router.route_prompt` routes to the
chosen worker; `local` = unchanged. Leased cloud machines (Visor
`GET /v1/machines`) are a future engine class (interface stubbed, not built).
Frontend picker is a TODO. Tested: `middleware_test/engine_selector_test.py`.
- **Chat Copilot** (`middleware/copilot.py` + `branding/hanzo-copilot.{js,css}`):
a sidebar chat that reads and MUTATES the workflow graph in natural language.
Backend `POST /v1/copilot/chat` proxies one OpenAI tool-calling round to a
chat endpoint (`STUDIO_COPILOT_URL`; default = local engine
`127.0.0.1:1234` if its port is open, else `api.hanzo.ai`; `STUDIO_COPILOT_MODEL`
default `zen`; token from `STUDIO_COPILOT_TOKEN`→`STUDIO_COMMERCE_TOKEN`→request
bearer). It returns `{reply, ops}`; `add_node` ops are validated against
`nodes.NODE_CLASS_MAPPINGS` server-side so the model can't invent node types.
The **frontend** applies ops against `app.graph` (LiteGraph) — op schema:
`add_node`(id?/pos?) · `set_widget` · `connect`(slots by name or index) ·
`set_prompt` · `move_node` · `delete_node` · `layout` (columnar tidy) ·
`queue`. Every op is guarded (unknown type/slot = skip + note, never throw).
Injected into the prebuilt frontend by `branding/patch_copilot.py` (copies the
bundle into static/, CSS `<link>` in `<head>`, deferred `<script>` before
`</body>`; idempotent, wired as step 9 of `apply-branding.sh`) — survives
frontend upgrades. `window.HanzoCopilot.applyOps` is exposed for tests. Tab
registered via `app.extensionManager.registerSidebarTab` (id `hanzo-copilot`).
Tested: `middleware_test/copilot_test.py` (op-validation + mocked tool loop) and
`branding/copilot_smoke.py` (headless: applier mutates a mock graph + panel
renders). Live end-to-end needs one server restart to register the route.
- **Identity user menu + session API** (`middleware/session.py` +
`branding/hanzo-identity.{js,css}`): the top-right user menu, driven entirely
by the IAM-validated token. Backend (thin, `/v1` house style, registered in
`add_routes` so it works auth-on and auth-off): `GET /v1/session` →
`{user,email,org,orgs[],project,projects[],console_url,iam_url,authenticated}`
read from `request["iam_user"]`; `POST /v1/session/org` sets the active org
(validated against membership → `studio_active_org` cookie); `POST
/v1/session/project` sets the workflow-workspace project scope; `GET|POST
/logout` clears the session + selection cookies and bounces to the IAM
end-session → `/login`. **Org switching is real, one hook**: the auth
middleware applies `session.resolve_org(...)` to a COPY of the validated user
(never the cached dict) so `iam_user["org_id"]` — the id every tenancy/output
path already reads — reflects the switch; membership comes from
`_orgs_from_claims` (`owner` + `organizations`/`orgs`/`groups`, list or CSV or
`{name}` dicts, home first, deduped). Frontend is a SPA-agnostic fixed pill +
dropdown (avatar/initials, user, org switcher, project switcher, Console
link → `console.hanzo.ai`, Logout); 401 → "Sign in with Hanzo" → `/login`.
Injected by `branding/patch_identity.py` (step 10 of `apply-branding.sh`,
same seam as the copilot). `window.HanzoIdentity` is exposed for tests.
Tested: `middleware_test/session_test.py` (resolve/orgs/routes/logout, real
aiohttp app) + `scratchpad/studio-iam/test_identity.py` (headless: pill,
dropdown, org switch re-scope, signed-out). The `/v1/session` routes need one
server restart to register (static + de-Comfy load without restart).
- **De-Comfy display strings** (`branding/patch_destring.py`, step 11 —
runs LAST): the systematic English brand-word sweep the curated phrase list in
`patch_frontend.py` structurally can't reach. Rule 1: whole-word `ComfyUI` →
`Hanzo Studio` only where it reads as a display word (preceded by
open-quote/space/`>`/`(`, followed by close-quote/space/sentence-punct) — that
boundary skips every code use in the bundle (`ComfyUIStarted`, `.ComfyUI`,
`=ComfyUI`, the `/^(ComfyUI-.../` regex, dotted i18n keys `.category.ComfyUI`)
and hyphenated product names (`ComfyUI-Manager`). Rule 2: curated bare-`Comfy`
**values** as exact `key:` value`` pairs so the Settings dialog **keys**
(`Comfy`, `Comfy-Desktop`, `Comfy.ColorPalette`) are preserved while the
displayed section header becomes **Studio** / **Studio Desktop** (this is the
"Settings must not say Comfy" fix) + `Comfy Cloud` → `Hanzo Cloud`. Scope:
English only — files above `LOCALE_RATIO` (0.15%) non-ASCII are skipped (all 33
locale bundles are ≥0.31%; English bundles ~0.02%) and each match needs a
pure-ASCII window, so the non-English `main-*/nodeDefs-*/commands-*` chunks are
left as-is (noted, out of scope). Idempotent. Verified: served English display
`ComfyUI` 63→0; a few example filesystem paths (`Documents/ComfyUI`) and the
external comfy.org service labels (`Comfy API Key`, `Comfy Node Registry`,
behind disabled API-nodes) are intentionally left. patch_frontend.py keeps its
orthogonal concern (URLs/domains + `Comfy-Org` → `Hanzo AI`).
## Tests
`tests-unit/{folder_paths_test,middleware_test,app_test,prompt_server_test}` —
run `uv run pytest tests-unit/folder_paths_test tests-unit/middleware_test
tests-unit/app_test tests-unit/prompt_server_test -q`. The auth/tenancy work is
covered by `middleware_test/iam_auth_test.py`,
`middleware_test/session_test.py` and
`folder_paths_test/org_scoping_test.py`.
+42
View File
@@ -0,0 +1,42 @@
Hanzo Studio
============
Hanzo Studio is a fork of ComfyUI (https://github.com/comfyanonymous/ComfyUI),
Copyright (c) ComfyUI contributors, licensed under the GNU General Public
License v3.0 (GPL-3.0).
Modifications Copyright (c) 2026 Hanzo Industries Inc, also released under the
GNU General Public License v3.0 (GPL-3.0). See the LICENSE file for the full
license text.
As required by the GPL-3.0, this fork remains under the same license as the
upstream project. The original ComfyUI copyright notices are retained.
Modifications made in this fork
-------------------------------
- Branding: user-visible name changed from "ComfyUI" to "Hanzo Studio",
including the served page <title>, favicon, in-app logos, and display
strings. The prebuilt frontend package (comfyui-frontend-package) is patched
at install time by branding/apply-branding.sh; source assets live in
branding/. Backend Python packages are namespaced under studio_* and the
server reports itself as "Hanzo Studio".
- Hanzo Engine integration nodes (custom_nodes/hanzo_engine/): first-party
nodes that run AI workloads on a local Hanzo Engine OpenAI-compatible server
over a shared client — HanzoChat (chat/completions, streaming), HanzoImageGen
(images/generations), HanzoTTS (audio/speech), HanzoASR
(audio/transcriptions), HanzoEngineRequest (generic /v1 escape hatch),
HanzoVisionCaption (vision chat/completions), HanzoTextToVideo (videos),
HanzoImageTo3D (3d), and HanzoSaveText. Hanzo Engine is the native-Rust,
all-modality inference and training backend.
- Fashion/swimwear starter workflows (user/default/workflows/fashion/):
product-shot, garment-edit, and caption-dataset workflow templates for a
designer's day-to-day use.
- Deployment/integration middleware for Hanzo infrastructure (IAM auth via
hanzo.id, commerce/billing, metrics), wired through environment variables and
the Dockerfile.
Third-party components retain their own upstream copyrights and licenses.
+7 -7
View File
@@ -1,4 +1,4 @@
# The Comfy guide to Quantization
# The Hanzo Studio guide to Quantization
## How does quantization work?
@@ -27,7 +27,7 @@ tensor_dq ~ tensor
Given that additional information (scaling factor) is needed to "interpret" the quantized values, we describe those as derived datatypes.
## Quantization in Comfy
## Quantization in Hanzo Studio
```
QuantizedTensor (torch.Tensor subclass)
@@ -39,7 +39,7 @@ MixedPrecisionOps + Metadata Detection
### Representation
To represent these derived datatypes, ComfyUI uses a subclass of torch.Tensor to implements these using the `QuantizedTensor` class found in `comfy/quant_ops.py`
To represent these derived datatypes, Hanzo Studio uses a subclass of torch.Tensor to implements these using the `QuantizedTensor` class found in `studio/quant_ops.py`
A `Layout` class defines how a specific quantization format behaves:
- Required parameters
@@ -47,7 +47,7 @@ A `Layout` class defines how a specific quantization format behaves:
- De-Quantize method
```python
from comfy.quant_ops import QuantizedLayout
from studio.quant_ops import QuantizedLayout
class MyLayout(QuantizedLayout):
@classmethod
@@ -67,7 +67,7 @@ The first is a **generic registry** that handles operations common to all quanti
The second registry is layout-specific and allows to implement fast-paths like nn.Linear.
```python
from comfy.quant_ops import register_layout_op
from studio.quant_ops import register_layout_op
@register_layout_op(torch.ops.aten.linear.default, MyLayout)
def my_linear(func, args, kwargs):
@@ -80,7 +80,7 @@ For any unsupported operation, QuantizedTensor will fallback to call `dequantize
### Mixed Precision
The `MixedPrecisionOps` class (lines 542-648 in `comfy/ops.py`) enables per-layer quantization decisions, allowing different layers in a model to use different precisions. This is activated when a model config contains a `layer_quant_config` dictionary that specifies which layers should be quantized and how.
The `MixedPrecisionOps` class (lines 542-648 in `studio/ops.py`) enables per-layer quantization decisions, allowing different layers in a model to use different precisions. This is activated when a model config contains a `layer_quant_config` dictionary that specifies which layers should be quantized and how.
**Architecture:**
@@ -125,7 +125,7 @@ We define 4 possible scaling parameters that should cover most recipes in the ne
|--------|---------------|--------------|----------------|-----------------|-------------|
| float8_e4m3fn | float32 | float32 (scalar) | - | - | float32 (scalar) |
You can find the defined formats in `comfy/quant_ops.py` (QUANT_ALGOS).
You can find the defined formats in `studio/quant_ops.py` (QUANT_ALGOS).
### Quantization Metadata
+72 -194
View File
@@ -1,92 +1,74 @@
<p align="center"><img src=".github/hero.svg" alt="studio" width="880"></p>
<div align="center">
# ComfyUI
# Hanzo Studio
**The most powerful and modular visual AI engine and application.**
[![Website][website-shield]][website-url]
[![Dynamic JSON Badge][discord-shield]][discord-url]
[![Twitter][twitter-shield]][twitter-url]
[![Matrix][matrix-shield]][matrix-url]
<br>
[![][github-release-shield]][github-release-link]
[![][github-release-date-shield]][github-release-link]
[![][github-downloads-shield]][github-downloads-link]
[![][github-downloads-latest-shield]][github-downloads-link]
[matrix-shield]: https://img.shields.io/badge/Matrix-000000?style=flat&logo=matrix&logoColor=white
[matrix-url]: https://app.element.io/#/room/%23comfyui_space%3Amatrix.org
[website-shield]: https://img.shields.io/badge/ComfyOrg-4285F4?style=flat
[website-url]: https://www.comfy.org/
[website-shield]: https://img.shields.io/badge/HanzoAI-4285F4?style=flat
[website-url]: https://www.hanzo.ai/
<!-- Workaround to display total user from https://github.com/badges/shields/issues/4500#issuecomment-2060079995 -->
[discord-shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fcomfyorg%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Discord&color=green&suffix=%20total
[discord-url]: https://www.comfy.org/discord
[twitter-shield]: https://img.shields.io/twitter/follow/ComfyUI
[twitter-url]: https://x.com/ComfyUI
[discord-shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Fhanzoai%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Discord&color=green&suffix=%20total
[discord-url]: https://discord.gg/hanzoai
[twitter-shield]: https://img.shields.io/twitter/follow/hanaboroai
[twitter-url]: https://x.com/hanzoai
[github-release-shield]: https://img.shields.io/github/v/release/comfyanonymous/ComfyUI?style=flat&sort=semver
[github-release-link]: https://github.com/comfyanonymous/ComfyUI/releases
[github-release-date-shield]: https://img.shields.io/github/release-date/comfyanonymous/ComfyUI?style=flat
[github-downloads-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/total?style=flat
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/comfyanonymous/ComfyUI/latest/total?style=flat&label=downloads%40latest
[github-downloads-link]: https://github.com/comfyanonymous/ComfyUI/releases
[github-release-shield]: https://img.shields.io/github/v/release/hanzoai/studio?style=flat&sort=semver
[github-release-link]: https://github.com/hanzoai/studio/releases
[github-release-date-shield]: https://img.shields.io/github/release-date/hanzoai/studio?style=flat
[github-downloads-shield]: https://img.shields.io/github/downloads/hanzoai/studio/total?style=flat
[github-downloads-latest-shield]: https://img.shields.io/github/downloads/hanzoai/studio/latest/total?style=flat&label=downloads%40latest
[github-downloads-link]: https://github.com/hanzoai/studio/releases
![ComfyUI Screenshot](https://github.com/user-attachments/assets/7ccaf2c1-9b72-41ae-9a89-5688c94b7abe)
![Hanzo Studio Screenshot](https://github.com/user-attachments/assets/7ccaf2c1-9b72-41ae-9a89-5688c94b7abe)
</div>
ComfyUI lets you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface. Available on Windows, Linux, and macOS.
Hanzo Studio lets you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface. Available on Windows, Linux, and macOS.
## Upstream & License
Hanzo Studio is a fork of [**ComfyUI**](https://github.com/comfyanonymous/ComfyUI) by comfyanonymous and the ComfyUI contributors — full credit to the upstream project for the engine this builds on. ComfyUI is licensed under the **GNU General Public License v3.0 (GPL-3.0)**, and this fork stays under the same license. All modifications in this fork are likewise GPL-3.0.
See [`LICENSE`](LICENSE) for the full license text and [`NOTICE`](NOTICE) for the fork's attribution and a summary of what it changes (branding, Hanzo Engine integration nodes, and fashion workflow templates).
## Get Started
#### [Desktop Application](https://www.comfy.org/download)
- The easiest way to get started.
- Available on Windows & macOS.
#### [Windows Portable Package](#installing)
- Get the latest commits and completely portable.
- Available on Windows.
#### [Manual Install](#manual-install-windows-linux)
Supports all operating systems and GPU types (NVIDIA, AMD, Intel, Apple Silicon, Ascend).
## [Examples](https://comfyanonymous.github.io/ComfyUI_examples/)
See what ComfyUI can do with the [example workflows](https://comfyanonymous.github.io/ComfyUI_examples/).
#### Docker (Recommended for servers)
```bash
docker pull ghcr.io/hanzoai/studio:latest
docker run -p 8188:8188 ghcr.io/hanzoai/studio:latest --listen --cpu
```
## Features
- Nodes/graph/flowchart interface to experiment and create complex Stable Diffusion workflows without needing to code anything.
- Image Models
- SD1.x, SD2.x ([unCLIP](https://comfyanonymous.github.io/ComfyUI_examples/unclip/))
- [SDXL](https://comfyanonymous.github.io/ComfyUI_examples/sdxl/), [SDXL Turbo](https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/)
- [Stable Cascade](https://comfyanonymous.github.io/ComfyUI_examples/stable_cascade/)
- [SD3 and SD3.5](https://comfyanonymous.github.io/ComfyUI_examples/sd3/)
- Pixart Alpha and Sigma
- [AuraFlow](https://comfyanonymous.github.io/ComfyUI_examples/aura_flow/)
- [HunyuanDiT](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_dit/)
- [Flux](https://comfyanonymous.github.io/ComfyUI_examples/flux/)
- [Lumina Image 2.0](https://comfyanonymous.github.io/ComfyUI_examples/lumina2/)
- [HiDream](https://comfyanonymous.github.io/ComfyUI_examples/hidream/)
- [Qwen Image](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/)
- [Hunyuan Image 2.1](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_image/)
- [Flux 2](https://comfyanonymous.github.io/ComfyUI_examples/flux2/)
- [Z Image](https://comfyanonymous.github.io/ComfyUI_examples/z_image/)
- SD1.x, SD2.x, SDXL, SDXL Turbo
- Stable Cascade, SD3 and SD3.5
- Pixart Alpha and Sigma, AuraFlow, HunyuanDiT
- Flux, Flux 2, Lumina Image 2.0, HiDream
- Qwen Image, Hunyuan Image 2.1, Z Image
- Image Editing Models
- [Omnigen 2](https://comfyanonymous.github.io/ComfyUI_examples/omnigen/)
- [Flux Kontext](https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-kontext-image-editing-model)
- [HiDream E1.1](https://comfyanonymous.github.io/ComfyUI_examples/hidream/#hidream-e11)
- [Qwen Image Edit](https://comfyanonymous.github.io/ComfyUI_examples/qwen_image/#edit-model)
- Omnigen 2, Flux Kontext, HiDream E1.1, Qwen Image Edit
- Video Models
- [Stable Video Diffusion](https://comfyanonymous.github.io/ComfyUI_examples/video/)
- [Mochi](https://comfyanonymous.github.io/ComfyUI_examples/mochi/)
- [LTX-Video](https://comfyanonymous.github.io/ComfyUI_examples/ltxv/)
- [Hunyuan Video](https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/)
- [Wan 2.1](https://comfyanonymous.github.io/ComfyUI_examples/wan/)
- [Wan 2.2](https://comfyanonymous.github.io/ComfyUI_examples/wan22/)
- [Hunyuan Video 1.5](https://docs.comfy.org/tutorials/video/hunyuan/hunyuan-video-1-5)
- Stable Video Diffusion, Mochi, LTX-Video
- Hunyuan Video, Wan 2.1, Wan 2.2, Hunyuan Video 1.5
- Audio Models
- [Stable Audio](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- [ACE Step](https://comfyanonymous.github.io/ComfyUI_examples/audio/)
- Stable Audio, ACE Step
- 3D Models
- [Hunyuan3D 2.0](https://docs.comfy.org/tutorials/3d/hunyuan3D-2)
- Hunyuan3D 2.0
- Asynchronous Queue system
- Many optimizations: Only re-executes the parts of the workflow that changes between executions.
- Smart memory management: can automatically run large models on GPUs with as low as 1GB vram with smart offloading.
@@ -94,45 +76,23 @@ See what ComfyUI can do with the [example workflows](https://comfyanonymous.gith
- Can load ckpt and safetensors: All in one checkpoints or standalone diffusion models, VAEs and CLIP models.
- Safe loading of ckpt, pt, pth, etc.. files.
- Embeddings/Textual inversion
- [Loras (regular, locon and loha)](https://comfyanonymous.github.io/ComfyUI_examples/lora/)
- [Hypernetworks](https://comfyanonymous.github.io/ComfyUI_examples/hypernetworks/)
- Loras (regular, locon and loha)
- Hypernetworks
- Loading full workflows (with seeds) from generated PNG, WebP and FLAC files.
- Saving/Loading workflows as Json files.
- Nodes interface can be used to create complex workflows like one for [Hires fix](https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/) or much more advanced ones.
- [Area Composition](https://comfyanonymous.github.io/ComfyUI_examples/area_composition/)
- [Inpainting](https://comfyanonymous.github.io/ComfyUI_examples/inpaint/) with both regular and inpainting models.
- [ControlNet and T2I-Adapter](https://comfyanonymous.github.io/ComfyUI_examples/controlnet/)
- [Upscale Models (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)](https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/)
- [GLIGEN](https://comfyanonymous.github.io/ComfyUI_examples/gligen/)
- [Model Merging](https://comfyanonymous.github.io/ComfyUI_examples/model_merging/)
- [LCM models and Loras](https://comfyanonymous.github.io/ComfyUI_examples/lcm/)
- Latent previews with [TAESD](#how-to-show-high-quality-previews)
- Nodes interface can be used to create complex workflows like one for Hires fix or much more advanced ones.
- Area Composition
- Inpainting with both regular and inpainting models.
- ControlNet and T2I-Adapter
- Upscale Models (ESRGAN, ESRGAN variants, SwinIR, Swin2SR, etc...)
- GLIGEN
- Model Merging
- LCM models and Loras
- Latent previews with TAESD
- Works fully offline: core will never download anything unless you want to.
- Optional API nodes to use paid models from external providers through the online [Comfy API](https://docs.comfy.org/tutorials/api-nodes/overview) disable with: `--disable-api-nodes`
- Optional API nodes to use paid models from external providers through the online API. Disable with: `--disable-api-nodes`
- [Config file](extra_model_paths.yaml.example) to set the search paths for models.
Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/)
## Release Process
ComfyUI follows a weekly release cycle targeting Monday but this regularly changes because of model releases or large changes to the codebase. There are three interconnected repositories:
1. **[ComfyUI Core](https://github.com/comfyanonymous/ComfyUI)**
- Releases a new stable version (e.g., v0.7.0) roughly every week.
- Starting from v0.4.0 patch versions will be used for fixes backported onto the current stable release.
- Minor versions will be used for releases off the master branch.
- Patch versions may still be used for releases on the master branch in cases where a backport would not make sense.
- Commits outside of the stable release tags may be very unstable and break many custom nodes.
- Serves as the foundation for the desktop release
2. **[ComfyUI Desktop](https://github.com/Comfy-Org/desktop)**
- Builds a new release using the latest stable core version
3. **[ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend)**
- Weekly frontend updates are merged into the core repository
- Features are frozen for the upcoming core release
- Development continues for the next release cycle
## Shortcuts
| Keybind | Explanation |
@@ -173,39 +133,6 @@ ComfyUI follows a weekly release cycle targeting Monday but this regularly chang
# Installing
## Windows Portable
There is a portable standalone build for Windows that should work for running on Nvidia GPUs or for running on your CPU only on the [releases page](https://github.com/comfyanonymous/ComfyUI/releases).
### [Direct link to download](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia.7z)
Simply download, extract with [7-Zip](https://7-zip.org) or with the windows explorer on recent windows versions and run. For smaller models you normally only need to put the checkpoints (the huge ckpt/safetensors files) in: ComfyUI\models\checkpoints but many of the larger models have multiple files. Make sure to follow the instructions to know which subfolder to put them in ComfyUI\models\
If you have trouble extracting it, right click the file -> properties -> unblock
The portable above currently comes with python 3.13 and pytorch cuda 13.0. Update your Nvidia drivers if it doesn't start.
#### Alternative Downloads:
[Experimental portable for AMD GPUs](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_amd.7z)
[Portable with pytorch cuda 12.8 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu128.7z).
[Portable with pytorch cuda 12.6 and python 3.12](https://github.com/comfyanonymous/ComfyUI/releases/latest/download/ComfyUI_windows_portable_nvidia_cu126.7z) (Supports Nvidia 10 series and older GPUs).
#### How do I share models between another UI and ComfyUI?
See the [Config file](extra_model_paths.yaml.example) to set the search paths for models. In the standalone windows build you can find this file in the ComfyUI directory. Rename this file to extra_model_paths.yaml and edit it with your favorite text editor.
## [comfy-cli](https://docs.comfy.org/comfy-cli/getting-started)
You can install and start ComfyUI using comfy-cli:
```bash
pip install comfy-cli
comfy install
```
## Manual Install (Windows, Linux)
Python 3.14 works but some custom nodes may have issues. The free threaded variant works but some dependencies will enable the GIL so it's not fully supported.
@@ -229,9 +156,9 @@ AMD users can install rocm and pytorch with pip if you don't have it already ins
```pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.1```
This is the command to install the nightly with ROCm 7.1 which might have some performance improvements:
This is the command to install the nightly with ROCm 7.2 which might have some performance improvements:
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm7.1```
```pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/rocm7.2```
### AMD GPUs (Experimental: Windows and Linux), RDNA 3, 3.5 and 4 only.
@@ -282,24 +209,24 @@ And install it again with the command above.
### Dependencies
Install the dependencies by opening your terminal inside the ComfyUI folder and:
Install the dependencies by opening your terminal inside the Hanzo Studio folder and:
```pip install -r requirements.txt```
After this you should have everything installed and can proceed to running ComfyUI.
After this you should have everything installed and can proceed to running Hanzo Studio.
### Others:
#### Apple Mac silicon
You can install ComfyUI in Apple Mac silicon (M1 or M2) with any recent macOS version.
You can install Hanzo Studio in Apple Mac silicon (M1 or M2) with any recent macOS version.
1. Install pytorch nightly. For instructions, read the [Accelerated PyTorch training on Mac](https://developer.apple.com/metal/pytorch/) Apple Developer guide (make sure to install the latest pytorch nightly).
1. Follow the [ComfyUI manual installation](#manual-install-windows-linux) instructions for Windows and Linux.
1. Install the ComfyUI [dependencies](#dependencies). If you have another Stable Diffusion UI [you might be able to reuse the dependencies](#i-already-have-another-ui-for-stable-diffusion-installed-do-i-really-have-to-install-all-of-these-dependencies).
1. Launch ComfyUI by running `python main.py`
1. Follow the [Hanzo Studio manual installation](#manual-install-windows-linux) instructions for Windows and Linux.
1. Install the Hanzo Studio [dependencies](#dependencies). If you have another Stable Diffusion UI you might be able to reuse the dependencies.
1. Launch Hanzo Studio by running `python main.py`
> **Note**: Remember to add your models, VAE, LoRAs etc. to the corresponding Comfy folders, as discussed in [ComfyUI manual installation](#manual-install-windows-linux).
> **Note**: Remember to add your models, VAE, LoRAs etc. to the corresponding folders, as discussed in [Hanzo Studio manual installation](#manual-install-windows-linux).
#### Ascend NPUs
@@ -308,7 +235,7 @@ For models compatible with Ascend Extension for PyTorch (torch_npu). To get star
1. Begin by installing the recommended or newer kernel version for Linux as specified in the Installation page of torch-npu, if necessary.
2. Proceed with the installation of Ascend Basekit, which includes the driver, firmware, and CANN, following the instructions provided for your specific platform.
3. Next, install the necessary packages for torch-npu by adhering to the platform-specific instructions on the [Installation](https://ascend.github.io/docs/sources/pytorch/install.html#pytorch) page.
4. Finally, adhere to the [ComfyUI manual installation](#manual-install-windows-linux) guide for Linux. Once all components are installed, you can run ComfyUI as described earlier.
4. Finally, adhere to the [Hanzo Studio manual installation](#manual-install-windows-linux) guide for Linux. Once all components are installed, you can run Hanzo Studio as described earlier.
#### Cambricon MLUs
@@ -316,19 +243,19 @@ For models compatible with Cambricon Extension for PyTorch (torch_mlu). Here's a
1. Install the Cambricon CNToolkit by adhering to the platform-specific instructions on the [Installation](https://www.cambricon.com/docs/sdk_1.15.0/cntoolkit_3.7.2/cntoolkit_install_3.7.2/index.html)
2. Next, install the PyTorch(torch_mlu) following the instructions on the [Installation](https://www.cambricon.com/docs/sdk_1.15.0/cambricon_pytorch_1.17.0/user_guide_1.9/index.html)
3. Launch ComfyUI by running `python main.py`
3. Launch Hanzo Studio by running `python main.py`
#### Iluvatar Corex
For models compatible with Iluvatar Extension for PyTorch. Here's a step-by-step guide tailored to your platform and installation method:
1. Install the Iluvatar Corex Toolkit by adhering to the platform-specific instructions on the [Installation](https://support.iluvatar.com/#/DocumentCentre?id=1&nameCenter=2&productId=520117912052801536)
2. Launch ComfyUI by running `python main.py`
2. Launch Hanzo Studio by running `python main.py`
## [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager/tree/manager-v4)
## Manager
**ComfyUI-Manager** is an extension that allows you to easily install, update, and manage custom nodes for ComfyUI.
The **Manager** extension allows you to easily install, update, and manage custom nodes for Hanzo Studio.
### Setup
@@ -337,7 +264,7 @@ For models compatible with Iluvatar Extension for PyTorch. Here's a step-by-step
pip install -r manager_requirements.txt
```
2. Enable the manager with the `--enable-manager` flag when running ComfyUI:
2. Enable the manager with the `--enable-manager` flag when running Hanzo Studio:
```bash
python main.py --enable-manager
```
@@ -346,7 +273,7 @@ For models compatible with Iluvatar Extension for PyTorch. Here's a step-by-step
| Flag | Description |
|------|-------------|
| `--enable-manager` | Enable ComfyUI-Manager |
| `--enable-manager` | Enable the Manager |
| `--enable-manager-legacy-ui` | Use the legacy manager UI instead of the new UI (requires `--enable-manager`) |
| `--disable-manager-ui` | Disable the manager UI and endpoints while keeping background features like security checks and scheduled installation completion (requires `--enable-manager`) |
@@ -365,7 +292,7 @@ For AMD 7600 and maybe other RDNA3 cards: ```HSA_OVERRIDE_GFX_VERSION=11.0.0 pyt
### AMD ROCm Tips
You can enable experimental memory efficient attention on recent pytorch in ComfyUI on some AMD GPUs using this command, it should already be enabled by default on RDNA3. If this improves speed for you on latest pytorch on your GPU please report it so that I can enable it by default.
You can enable experimental memory efficient attention on recent pytorch on some AMD GPUs using this command, it should already be enabled by default on RDNA3. If this improves speed for you on latest pytorch on your GPU please report it so that we can enable it by default.
```TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL=1 python main.py --use-pytorch-cross-attention```
@@ -379,9 +306,9 @@ Only parts of the graph that change from each execution to the next will be exec
Dragging a generated png on the webpage or loading one will give you the full workflow including seeds that were used to create it.
You can use () to change emphasis of a word or phrase like: (good code:1.2) or (bad code:0.8). The default emphasis for () is 1.1. To use () characters in your actual prompt escape them like \\( or \\).
You can use () to change emphasis of a word or phrase like: (good code:1.2) or (bad code:0.8). The default emphasis for () is 1.1. To use () characters in your actual prompt escape them like \( or \).
You can use {day|night}, for wildcard/dynamic prompts. With this syntax "{wild|card|test}" will be randomly replaced by either "wild", "card" or "test" by the frontend every time you queue the prompt. To use {} characters in your actual prompt escape them like: \\{ or \\}.
You can use {day|night}, for wildcard/dynamic prompts. With this syntax "{wild|card|test}" will be randomly replaced by either "wild", "card" or "test" by the frontend every time you queue the prompt. To use {} characters in your actual prompt escape them like: \{ or \}.
Dynamic prompts also support C-style comments, like `// comment` or `/* comment */`.
@@ -394,7 +321,7 @@ To use a textual inversion concepts/embeddings in a text prompt put them in the
Use ```--preview-method auto``` to enable previews.
The default installation includes a fast latent preview method that's low-resolution. To enable higher-quality previews with [TAESD](https://github.com/madebyollin/taesd), download the [taesd_decoder.pth, taesdxl_decoder.pth, taesd3_decoder.pth and taef1_decoder.pth](https://github.com/madebyollin/taesd/) and place them in the `models/vae_approx` folder. Once they're installed, restart ComfyUI and launch it with `--preview-method taesd` to enable high-quality previews.
The default installation includes a fast latent preview method that's low-resolution. To enable higher-quality previews with [TAESD](https://github.com/madebyollin/taesd), download the [taesd_decoder.pth, taesdxl_decoder.pth, taesd3_decoder.pth and taef1_decoder.pth](https://github.com/madebyollin/taesd/) and place them in the `models/vae_approx` folder. Once they're installed, restart Hanzo Studio and launch it with `--preview-method taesd` to enable high-quality previews.
## How to use TLS/SSL?
Generate a self-signed certificate (not appropriate for shared/production use) and key by running the command: `openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"`
@@ -406,55 +333,6 @@ Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app w
## Support and dev channel
[Discord](https://comfy.org/discord): Try the #help or #feedback channels.
[Discord](https://discord.gg/hanzoai): Try the #help or #feedback channels.
[Matrix space: #comfyui_space:matrix.org](https://app.element.io/#/room/%23comfyui_space%3Amatrix.org) (it's like discord but open source).
See also: [https://www.comfy.org/](https://www.comfy.org/)
## Frontend Development
As of August 15, 2024, we have transitioned to a new frontend, which is now hosted in a separate repository: [ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend). This repository now hosts the compiled JS (from TS/Vue) under the `web/` directory.
### Reporting Issues and Requesting Features
For any bugs, issues, or feature requests related to the frontend, please use the [ComfyUI Frontend repository](https://github.com/Comfy-Org/ComfyUI_frontend). This will help us manage and address frontend-specific concerns more efficiently.
### Using the Latest Frontend
The new frontend is now the default for ComfyUI. However, please note:
1. The frontend in the main ComfyUI repository is updated fortnightly.
2. Daily releases are available in the separate frontend repository.
To use the most up-to-date frontend version:
1. For the latest daily release, launch ComfyUI with this command line argument:
```
--front-end-version Comfy-Org/ComfyUI_frontend@latest
```
2. For a specific version, replace `latest` with the desired version number:
```
--front-end-version Comfy-Org/ComfyUI_frontend@1.2.2
```
This approach allows you to easily switch between the stable fortnightly release and the cutting-edge daily updates, or even specific versions for testing purposes.
### Accessing the Legacy Frontend
If you need to use the legacy frontend for any reason, you can access it using the following command line argument:
```
--front-end-version Comfy-Org/ComfyUI_legacy_frontend@latest
```
This will use a snapshot of the legacy frontend preserved in the [ComfyUI Legacy Frontend repository](https://github.com/Comfy-Org/ComfyUI_legacy_frontend).
# QA
### Which GPU should I buy for this?
[See this page for some recommendations](https://github.com/comfyanonymous/ComfyUI/wiki/Which-GPU-should-I-buy-for-ComfyUI)
See also: [https://www.hanzo.ai/](https://www.hanzo.ai/)
+1 -1
View File
@@ -63,7 +63,7 @@ version_path_separator = os
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///user/comfyui.db
sqlalchemy.url = sqlite:///user/hanzo_studio.db
[post_write_hooks]
+2 -2
View File
@@ -1,3 +1,3 @@
# ComfyUI Internal Routes
# Hanzo Studio Internal Routes
All routes under the `/internal` path are designated for **internal use by ComfyUI only**. These routes are not intended for use by external applications may change at any time without notice.
All routes under the `/internal` path are designated for **internal use by Hanzo Studio only**. These routes are not intended for use by external applications may change at any time without notice.
@@ -8,7 +8,7 @@ import os
class InternalRoutes:
'''
The top level web router for internal routes: /internal/*
The endpoints here should NOT be depended upon. It is for ComfyUI frontend use only.
The endpoints here should NOT be depended upon. It is for Hanzo Studio frontend use only.
Check README.md for more information.
'''
+2 -2
View File
@@ -12,7 +12,7 @@ class AppSettings():
try:
file = self.user_manager.get_request_user_filepath(
request,
"comfy.settings.json"
"studio.settings.json"
)
except KeyError as e:
logging.error("User settings not found.")
@@ -29,7 +29,7 @@ class AppSettings():
def save_settings(self, request, settings):
file = self.user_manager.get_request_user_filepath(
request, "comfy.settings.json")
request, "studio.settings.json")
with open(file, "w") as f:
f.write(json.dumps(settings, indent=4))
+7 -7
View File
@@ -37,7 +37,7 @@ def list_tree(base_dir: str) -> list[str]:
def prefixes_for_root(root: RootType) -> list[str]:
if root == "models":
bases: list[str] = []
for _bucket, paths in get_comfy_models_folders():
for _bucket, paths in get_studio_models_folders():
bases.extend(paths)
return [os.path.abspath(p) for p in bases]
if root == "input":
@@ -74,16 +74,16 @@ def utcnow() -> datetime:
"""Naive UTC timestamp (no tzinfo). We always treat DB datetimes as UTC."""
return datetime.now(timezone.utc).replace(tzinfo=None)
def get_comfy_models_folders() -> list[tuple[str, list[str]]]:
def get_studio_models_folders() -> list[tuple[str, list[str]]]:
"""Build a list of (folder_name, base_paths[]) categories that are configured for model locations.
We trust `folder_paths.folder_names_and_paths` and include a category if
*any* of its base paths lies under the Comfy `models_dir`.
*any* of its base paths lies under the Studio `models_dir`.
"""
targets: list[tuple[str, list[str]]] = []
models_root = os.path.abspath(folder_paths.models_dir)
for name, values in folder_paths.folder_names_and_paths.items():
paths, _exts = values[0], values[1] # NOTE: this prevents nodepacks that hackily edit folder_... from breaking ComfyUI
paths, _exts = values[0], values[1] # NOTE: this prevents nodepacks that hackily edit folder_... from breaking Hanzo Studio
if any(os.path.abspath(p).startswith(models_root + os.sep) for p in paths):
targets.append((name, paths))
return targets
@@ -152,7 +152,7 @@ def get_relative_to_root_category_path_of_asset(file_path: str) -> tuple[Literal
"""Given an absolute or relative file path, determine which root category the path belongs to:
- 'input' if the file resides under `folder_paths.get_input_directory()`
- 'output' if the file resides under `folder_paths.get_output_directory()`
- 'models' if the file resides under any base path of categories returned by `get_comfy_models_folders()`
- 'models' if the file resides under any base path of categories returned by `get_studio_models_folders()`
Returns:
(root_category, relative_path_inside_that_root)
@@ -185,7 +185,7 @@ def get_relative_to_root_category_path_of_asset(file_path: str) -> tuple[Literal
# 3) models (check deepest matching base to avoid ambiguity)
best: tuple[int, str, str] | None = None # (base_len, bucket, rel_inside_bucket)
for bucket, bases in get_comfy_models_folders():
for bucket, bases in get_studio_models_folders():
for b in bases:
base_abs = os.path.abspath(b)
if not _is_within(fp_abs, base_abs):
@@ -232,7 +232,7 @@ def normalize_tags(tags: list[str] | None) -> list[str]:
def collect_models_files() -> list[str]:
out: list[str] = []
for folder_name, bases in get_comfy_models_folders():
for folder_name, bases in get_studio_models_folders():
rel_files = folder_paths.get_filename_list(folder_name) or []
for rel_path in rel_files:
abs_path = folder_paths.get_full_path(folder_name, rel_path)
+2 -2
View File
@@ -3,7 +3,7 @@ import os
import shutil
from app.logger import log_startup_warning
from utils.install_util import get_missing_requirements_message
from comfy.cli_args import args
from studio.cli_args import args
_DB_AVAILABLE = False
Session = None
@@ -24,7 +24,7 @@ except ImportError as e:
------------------------------------------------------------------------
Error importing dependencies: {e}
{get_missing_requirements_message()}
This error is happening because ComfyUI now uses a local sqlite database.
This error is happening because Hanzo Studio now uses a local sqlite database.
------------------------------------------------------------------------
""".strip()
)
+3 -3
View File
@@ -19,7 +19,7 @@ from typing_extensions import NotRequired
from utils.install_util import get_missing_requirements_message, requirements_path
from comfy.cli_args import DEFAULT_VERSION_STRING
from studio.cli_args import DEFAULT_VERSION_STRING
import app.logger
@@ -27,7 +27,7 @@ def frontend_install_warning_message():
return f"""
{get_missing_requirements_message()}
This error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead.
This error is happening because the Hanzo Studio frontend is no longer shipped as part of the main repo but as a pip package instead.
""".strip()
def parse_version(version: str) -> tuple[int, int, int]:
@@ -87,7 +87,7 @@ ________________________________________________________________________
""".strip()
)
else:
logging.info("ComfyUI frontend version: {}".format(frontend_version_str))
logging.info("Hanzo Studio frontend version: {}".format(frontend_version_str))
except Exception as e:
logging.error(f"Failed to check frontend version: {e}")
+2 -2
View File
@@ -7,7 +7,7 @@ import time
import logging
import folder_paths
import glob
import comfy.utils
import studio.utils
from aiohttp import web
from PIL import Image
from io import BytesIO
@@ -180,7 +180,7 @@ class ModelFileManager:
if safetensors_file:
safetensors_filepath = os.path.join(dirname, safetensors_file)
header = comfy.utils.safetensors_header(safetensors_filepath, max_size=8*1024*1024)
header = studio.utils.safetensors_header(safetensors_filepath, max_size=8*1024*1024)
if header:
safetensors_metadata = json.loads(header)
safetensors_images = safetensors_metadata.get("__metadata__", {}).get("ssmd_cover_images", None)
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
from aiohttp import web
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
from studio_api.latest._io_public import NodeReplace
from studio_execution.graph_utils import is_link
import nodes
class NodeStruct(TypedDict):
inputs: dict[str, str | int | float | bool | tuple[str, int]]
class_type: str
_meta: dict[str, str]
def copy_node_struct(node_struct: NodeStruct, empty_inputs: bool = False) -> NodeStruct:
new_node_struct = node_struct.copy()
if empty_inputs:
new_node_struct["inputs"] = {}
else:
new_node_struct["inputs"] = node_struct["inputs"].copy()
new_node_struct["_meta"] = node_struct["_meta"].copy()
return new_node_struct
class NodeReplaceManager:
"""Manages node replacement registrations."""
def __init__(self):
self._replacements: dict[str, list[NodeReplace]] = {}
def register(self, node_replace: NodeReplace):
"""Register a node replacement mapping."""
self._replacements.setdefault(node_replace.old_node_id, []).append(node_replace)
def get_replacement(self, old_node_id: str) -> list[NodeReplace] | None:
"""Get replacements for an old node ID."""
return self._replacements.get(old_node_id)
def has_replacement(self, old_node_id: str) -> bool:
"""Check if a replacement exists for an old node ID."""
return old_node_id in self._replacements
def apply_replacements(self, prompt: dict[str, NodeStruct]):
connections: dict[str, list[tuple[str, str, int]]] = {}
need_replacement: set[str] = set()
for node_number, node_struct in prompt.items():
class_type = node_struct["class_type"]
# need replacement if not in NODE_CLASS_MAPPINGS and has replacement
if class_type not in nodes.NODE_CLASS_MAPPINGS.keys() and self.has_replacement(class_type):
need_replacement.add(node_number)
# keep track of connections
for input_id, input_value in node_struct["inputs"].items():
if is_link(input_value):
conn_number = input_value[0]
connections.setdefault(conn_number, []).append((node_number, input_id, input_value[1]))
for node_number in need_replacement:
node_struct = prompt[node_number]
class_type = node_struct["class_type"]
replacements = self.get_replacement(class_type)
if replacements is None:
continue
# just use the first replacement
replacement = replacements[0]
new_node_id = replacement.new_node_id
# if replacement is not a valid node, skip trying to replace it as will only cause confusion
if new_node_id not in nodes.NODE_CLASS_MAPPINGS.keys():
continue
# first, replace node id (class_type)
new_node_struct = copy_node_struct(node_struct, empty_inputs=True)
new_node_struct["class_type"] = new_node_id
# TODO: consider replacing display_name in _meta as well for error reporting purposes; would need to query node schema
# second, replace inputs
if replacement.input_mapping is not None:
for input_map in replacement.input_mapping:
if "set_value" in input_map:
new_node_struct["inputs"][input_map["new_id"]] = input_map["set_value"]
elif "old_id" in input_map:
new_node_struct["inputs"][input_map["new_id"]] = node_struct["inputs"][input_map["old_id"]]
# finalize input replacement
prompt[node_number] = new_node_struct
# third, replace outputs
if replacement.output_mapping is not None:
# re-mapping outputs requires changing the input values of nodes that receive connections from this one
if node_number in connections:
for conns in connections[node_number]:
conn_node_number, conn_input_id, old_output_idx = conns
for output_map in replacement.output_mapping:
if output_map["old_idx"] == old_output_idx:
new_output_idx = output_map["new_idx"]
previous_input = prompt[conn_node_number]["inputs"][conn_input_id]
previous_input[1] = new_output_idx
def as_dict(self):
"""Serialize all replacements to dict."""
return {
k: [v.as_dict() for v in v_list]
for k, v_list in self._replacements.items()
}
def add_routes(self, routes):
@routes.get("/node_replacements")
async def get_node_replacements(request):
return web.json_response(self.as_dict())
+2 -2
View File
@@ -53,7 +53,7 @@ class SubgraphManager:
return entry_id, entry
async def load_entry_data(self, entry: SubgraphEntry):
with open(entry['path'], 'r') as f:
with open(entry['path'], 'r', encoding='utf-8') as f:
entry['data'] = f.read()
return entry
@@ -100,7 +100,7 @@ class SubgraphManager:
if os.path.exists(blueprints_dir):
for file in glob.glob(os.path.join(blueprints_dir, "*.json")):
file = file.replace('\\', '/')
entry_id, entry = self._create_entry(file, Source.templates, "comfyui")
entry_id, entry = self._create_entry(file, Source.templates, "studio")
subgraphs_dict[entry_id] = entry
self.cached_blueprint_subgraphs = subgraphs_dict
+28 -5
View File
@@ -8,7 +8,7 @@ import shutil
import logging
from aiohttp import web
from urllib import parse
from comfy.cli_args import args
from studio.cli_args import args
import folder_paths
from .app_settings import AppSettings
from typing import TypedDict
@@ -57,8 +57,22 @@ class UserManager():
def get_request_user_id(self, request):
user = "default"
if args.multi_user and "comfy-user" in request.headers:
user = request.headers["comfy-user"]
# If IAM auth is active, extract user from IAM context
iam_user = request.get("iam_user")
if iam_user and iam_user.get("sub") and iam_user["sub"] != "local":
user = iam_user["sub"]
# A token subject must never escape into a System User namespace.
if user.startswith(folder_paths.SYSTEM_USER_PREFIX):
raise KeyError("Unknown user: " + user)
# Auto-register IAM users
if user not in self.users:
display = iam_user.get("name") or iam_user.get("email") or user
self.users[user] = display
return user
if args.multi_user and "studio-user" in request.headers:
user = request.headers["studio-user"]
# Block System Users (use same error message to prevent probing)
if user.startswith(folder_paths.SYSTEM_USER_PREFIX):
raise KeyError("Unknown user: " + user)
@@ -68,14 +82,23 @@ class UserManager():
return user
def get_request_org_id(self, request) -> str | None:
"""Extract org_id from IAM context or fallback to CLI arg."""
iam_user = request.get("iam_user")
if iam_user and iam_user.get("org_id"):
return iam_user["org_id"]
return folder_paths.get_org_id()
def get_request_user_filepath(self, request, file, type="userdata", create_dir=True):
if type == "userdata":
root_dir = folder_paths.get_user_directory()
# Use org-scoped user directory if multi-tenant
org_id = self.get_request_org_id(request)
root_dir = folder_paths.get_org_user_directory(org_id)
else:
raise KeyError("Unknown filepath type:" + type)
user = self.get_request_user_id(request)
user_root = folder_paths.get_public_user_directory(user)
user_root = folder_paths.get_public_user_directory(user, org_id)
if user_root is None:
return None
path = user_root
@@ -0,0 +1,44 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform float u_float0; // Brightness slider -100..100
uniform float u_float1; // Contrast slider -100..100
in vec2 v_texCoord;
out vec4 fragColor;
const float MID_GRAY = 0.18; // 18% reflectance
// sRGB gamma 2.2 approximation
vec3 srgbToLinear(vec3 c) {
return pow(max(c, 0.0), vec3(2.2));
}
vec3 linearToSrgb(vec3 c) {
return pow(max(c, 0.0), vec3(1.0/2.2));
}
float mapBrightness(float b) {
return clamp(b / 100.0, -1.0, 1.0);
}
float mapContrast(float c) {
return clamp(c / 100.0 + 1.0, 0.0, 2.0);
}
void main() {
vec4 orig = texture(u_image0, v_texCoord);
float brightness = mapBrightness(u_float0);
float contrast = mapContrast(u_float1);
vec3 lin = srgbToLinear(orig.rgb);
lin = (lin - MID_GRAY) * contrast + brightness + MID_GRAY;
// Convert back to sRGB
vec3 result = linearToSrgb(clamp(lin, 0.0, 1.0));
fragColor = vec4(result, orig.a);
}
@@ -0,0 +1,72 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform int u_int0; // Mode
uniform float u_float0; // Amount (0 to 100)
in vec2 v_texCoord;
out vec4 fragColor;
const int MODE_LINEAR = 0;
const int MODE_RADIAL = 1;
const int MODE_BARREL = 2;
const int MODE_SWIRL = 3;
const int MODE_DIAGONAL = 4;
const float AMOUNT_SCALE = 0.0005;
const float RADIAL_MULT = 4.0;
const float BARREL_MULT = 8.0;
const float INV_SQRT2 = 0.70710678118;
void main() {
vec2 uv = v_texCoord;
vec4 original = texture(u_image0, uv);
float amount = u_float0 * AMOUNT_SCALE;
if (amount < 0.000001) {
fragColor = original;
return;
}
// Aspect-corrected coordinates for circular effects
float aspect = u_resolution.x / u_resolution.y;
vec2 centered = uv - 0.5;
vec2 corrected = vec2(centered.x * aspect, centered.y);
float r = length(corrected);
vec2 dir = r > 0.0001 ? corrected / r : vec2(0.0);
vec2 offset = vec2(0.0);
if (u_int0 == MODE_LINEAR) {
// Horizontal shift (no aspect correction needed)
offset = vec2(amount, 0.0);
}
else if (u_int0 == MODE_RADIAL) {
// Outward from center, stronger at edges
offset = dir * r * amount * RADIAL_MULT;
offset.x /= aspect; // Convert back to UV space
}
else if (u_int0 == MODE_BARREL) {
// Lens distortion simulation (r² falloff)
offset = dir * r * r * amount * BARREL_MULT;
offset.x /= aspect; // Convert back to UV space
}
else if (u_int0 == MODE_SWIRL) {
// Perpendicular to radial (rotational aberration)
vec2 perp = vec2(-dir.y, dir.x);
offset = perp * r * amount * RADIAL_MULT;
offset.x /= aspect; // Convert back to UV space
}
else if (u_int0 == MODE_DIAGONAL) {
// 45° offset (no aspect correction needed)
offset = vec2(amount, amount) * INV_SQRT2;
}
float red = texture(u_image0, uv + offset).r;
float green = original.g;
float blue = texture(u_image0, uv - offset).b;
fragColor = vec4(red, green, blue, original.a);
}
+78
View File
@@ -0,0 +1,78 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform float u_float0; // temperature (-100 to 100)
uniform float u_float1; // tint (-100 to 100)
uniform float u_float2; // vibrance (-100 to 100)
uniform float u_float3; // saturation (-100 to 100)
in vec2 v_texCoord;
out vec4 fragColor;
const float INPUT_SCALE = 0.01;
const float TEMP_TINT_PRIMARY = 0.3;
const float TEMP_TINT_SECONDARY = 0.15;
const float VIBRANCE_BOOST = 2.0;
const float SATURATION_BOOST = 2.0;
const float SKIN_PROTECTION = 0.5;
const float EPSILON = 0.001;
const vec3 LUMA_WEIGHTS = vec3(0.299, 0.587, 0.114);
void main() {
vec4 tex = texture(u_image0, v_texCoord);
vec3 color = tex.rgb;
// Scale inputs: -100/100 → -1/1
float temperature = u_float0 * INPUT_SCALE;
float tint = u_float1 * INPUT_SCALE;
float vibrance = u_float2 * INPUT_SCALE;
float saturation = u_float3 * INPUT_SCALE;
// Temperature (warm/cool): positive = warm, negative = cool
color.r += temperature * TEMP_TINT_PRIMARY;
color.b -= temperature * TEMP_TINT_PRIMARY;
// Tint (green/magenta): positive = green, negative = magenta
color.g += tint * TEMP_TINT_PRIMARY;
color.r -= tint * TEMP_TINT_SECONDARY;
color.b -= tint * TEMP_TINT_SECONDARY;
// Single clamp after temperature/tint
color = clamp(color, 0.0, 1.0);
// Vibrance with skin protection
if (vibrance != 0.0) {
float maxC = max(color.r, max(color.g, color.b));
float minC = min(color.r, min(color.g, color.b));
float sat = maxC - minC;
float gray = dot(color, LUMA_WEIGHTS);
if (vibrance < 0.0) {
// Desaturate: -100 → gray
color = mix(vec3(gray), color, 1.0 + vibrance);
} else {
// Boost less saturated colors more
float vibranceAmt = vibrance * (1.0 - sat);
// Branchless skin tone protection
float isWarmTone = step(color.b, color.g) * step(color.g, color.r);
float warmth = (color.r - color.b) / max(maxC, EPSILON);
float skinTone = isWarmTone * warmth * sat * (1.0 - sat);
vibranceAmt *= (1.0 - skinTone * SKIN_PROTECTION);
color = mix(vec3(gray), color, 1.0 + vibranceAmt * VIBRANCE_BOOST);
}
}
// Saturation
if (saturation != 0.0) {
float gray = dot(color, LUMA_WEIGHTS);
float satMix = saturation < 0.0
? 1.0 + saturation // -100 → gray
: 1.0 + saturation * SATURATION_BOOST; // +100 → 3x boost
color = mix(vec3(gray), color, satMix);
}
fragColor = vec4(clamp(color, 0.0, 1.0), tex.a);
}
@@ -0,0 +1,94 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform float u_float0; // Blur radius (020, default ~5)
uniform float u_float1; // Edge threshold (0100, default ~30)
uniform int u_int0; // Step size (0/1 = every pixel, 2+ = skip pixels)
in vec2 v_texCoord;
out vec4 fragColor;
const int MAX_RADIUS = 20;
const float EPSILON = 0.0001;
// Perceptual luminance
float getLuminance(vec3 rgb) {
return dot(rgb, vec3(0.299, 0.587, 0.114));
}
vec4 bilateralFilter(vec2 uv, vec2 texelSize, int radius,
float sigmaSpatial, float sigmaColor)
{
vec4 center = texture(u_image0, uv);
vec3 centerRGB = center.rgb;
float invSpatial2 = -0.5 / (sigmaSpatial * sigmaSpatial);
float invColor2 = -0.5 / (sigmaColor * sigmaColor + EPSILON);
vec3 sumRGB = vec3(0.0);
float sumWeight = 0.0;
int step = max(u_int0, 1);
float radius2 = float(radius * radius);
for (int dy = -MAX_RADIUS; dy <= MAX_RADIUS; dy++) {
if (dy < -radius || dy > radius) continue;
if (abs(dy) % step != 0) continue;
for (int dx = -MAX_RADIUS; dx <= MAX_RADIUS; dx++) {
if (dx < -radius || dx > radius) continue;
if (abs(dx) % step != 0) continue;
vec2 offset = vec2(float(dx), float(dy));
float dist2 = dot(offset, offset);
if (dist2 > radius2) continue;
vec3 sampleRGB = texture(u_image0, uv + offset * texelSize).rgb;
// Spatial Gaussian
float spatialWeight = exp(dist2 * invSpatial2);
// Perceptual color distance (weighted RGB)
vec3 diff = sampleRGB - centerRGB;
float colorDist = dot(diff * diff, vec3(0.299, 0.587, 0.114));
float colorWeight = exp(colorDist * invColor2);
float w = spatialWeight * colorWeight;
sumRGB += sampleRGB * w;
sumWeight += w;
}
}
vec3 resultRGB = sumRGB / max(sumWeight, EPSILON);
return vec4(resultRGB, center.a); // preserve center alpha
}
void main() {
vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));
float radiusF = clamp(u_float0, 0.0, float(MAX_RADIUS));
int radius = int(radiusF + 0.5);
if (radius == 0) {
fragColor = texture(u_image0, v_texCoord);
return;
}
// Edge threshold → color sigma
// Squared curve for better low-end control
float t = clamp(u_float1, 0.0, 100.0) / 100.0;
t *= t;
float sigmaColor = mix(0.01, 0.5, t);
// Spatial sigma tied to radius
float sigmaSpatial = max(radiusF * 0.75, 0.5);
fragColor = bilateralFilter(
v_texCoord,
texelSize,
radius,
sigmaSpatial,
sigmaColor
);
}
+124
View File
@@ -0,0 +1,124 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform float u_float0; // grain amount [0.0 1.0] typical: 0.20.8
uniform float u_float1; // grain size [0.3 3.0] lower = finer grain
uniform float u_float2; // color amount [0.0 1.0] 0 = monochrome, 1 = RGB grain
uniform float u_float3; // luminance bias [0.0 1.0] 0 = uniform, 1 = shadows only
uniform int u_int0; // noise mode [0 or 1] 0 = smooth, 1 = grainy
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
// High-quality integer hash (pcg-like)
uint pcg(uint v) {
uint state = v * 747796405u + 2891336453u;
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
return (word >> 22u) ^ word;
}
// 2D -> 1D hash input
uint hash2d(uvec2 p) {
return pcg(p.x + pcg(p.y));
}
// Hash to float [0, 1]
float hashf(uvec2 p) {
return float(hash2d(p)) / float(0xffffffffu);
}
// Hash to float with offset (for RGB channels)
float hashf(uvec2 p, uint offset) {
return float(pcg(hash2d(p) + offset)) / float(0xffffffffu);
}
// Convert uniform [0,1] to roughly Gaussian distribution
// Using simple approximation: average of multiple samples
float toGaussian(uvec2 p) {
float sum = hashf(p, 0u) + hashf(p, 1u) + hashf(p, 2u) + hashf(p, 3u);
return (sum - 2.0) * 0.7; // Centered, scaled
}
float toGaussian(uvec2 p, uint offset) {
float sum = hashf(p, offset) + hashf(p, offset + 1u)
+ hashf(p, offset + 2u) + hashf(p, offset + 3u);
return (sum - 2.0) * 0.7;
}
// Smooth noise with better interpolation
float smoothNoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Quintic interpolation (less banding than cubic)
f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
uvec2 ui = uvec2(i);
float a = toGaussian(ui);
float b = toGaussian(ui + uvec2(1u, 0u));
float c = toGaussian(ui + uvec2(0u, 1u));
float d = toGaussian(ui + uvec2(1u, 1u));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float smoothNoise(vec2 p, uint offset) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
uvec2 ui = uvec2(i);
float a = toGaussian(ui, offset);
float b = toGaussian(ui + uvec2(1u, 0u), offset);
float c = toGaussian(ui + uvec2(0u, 1u), offset);
float d = toGaussian(ui + uvec2(1u, 1u), offset);
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
void main() {
vec4 color = texture(u_image0, v_texCoord);
// Luminance (Rec.709)
float luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
// Grain UV (resolution-independent)
vec2 grainUV = v_texCoord * u_resolution / max(u_float1, 0.01);
uvec2 grainPixel = uvec2(grainUV);
float g;
vec3 grainRGB;
if (u_int0 == 1) {
// Grainy mode: pure hash noise (no interpolation = no banding)
g = toGaussian(grainPixel);
grainRGB = vec3(
toGaussian(grainPixel, 100u),
toGaussian(grainPixel, 200u),
toGaussian(grainPixel, 300u)
);
} else {
// Smooth mode: interpolated with quintic curve
g = smoothNoise(grainUV);
grainRGB = vec3(
smoothNoise(grainUV, 100u),
smoothNoise(grainUV, 200u),
smoothNoise(grainUV, 300u)
);
}
// Luminance weighting (less grain in highlights)
float lumWeight = mix(1.0, 1.0 - luma, clamp(u_float3, 0.0, 1.0));
// Strength
float strength = u_float0 * 0.15;
// Color vs monochrome grain
vec3 grainColor = mix(vec3(g), grainRGB, clamp(u_float2, 0.0, 1.0));
color.rgb += grainColor * strength * lumWeight;
fragColor0 = vec4(clamp(color.rgb, 0.0, 1.0), color.a);
}
+133
View File
@@ -0,0 +1,133 @@
#version 300 es
precision mediump float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform int u_int0; // Blend mode
uniform int u_int1; // Color tint
uniform float u_float0; // Intensity
uniform float u_float1; // Radius
uniform float u_float2; // Threshold
in vec2 v_texCoord;
out vec4 fragColor;
const int BLEND_ADD = 0;
const int BLEND_SCREEN = 1;
const int BLEND_SOFT = 2;
const int BLEND_OVERLAY = 3;
const int BLEND_LIGHTEN = 4;
const float GOLDEN_ANGLE = 2.39996323;
const int MAX_SAMPLES = 48;
const vec3 LUMA = vec3(0.299, 0.587, 0.114);
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
vec3 hexToRgb(int h) {
return vec3(
float((h >> 16) & 255),
float((h >> 8) & 255),
float(h & 255)
) * (1.0 / 255.0);
}
vec3 blend(vec3 base, vec3 glow, int mode) {
if (mode == BLEND_SCREEN) {
return 1.0 - (1.0 - base) * (1.0 - glow);
}
if (mode == BLEND_SOFT) {
return mix(
base - (1.0 - 2.0 * glow) * base * (1.0 - base),
base + (2.0 * glow - 1.0) * (sqrt(base) - base),
step(0.5, glow)
);
}
if (mode == BLEND_OVERLAY) {
return mix(
2.0 * base * glow,
1.0 - 2.0 * (1.0 - base) * (1.0 - glow),
step(0.5, base)
);
}
if (mode == BLEND_LIGHTEN) {
return max(base, glow);
}
return base + glow;
}
void main() {
vec4 original = texture(u_image0, v_texCoord);
float intensity = u_float0 * 0.05;
float radius = u_float1 * u_float1 * 0.012;
if (intensity < 0.001 || radius < 0.1) {
fragColor = original;
return;
}
float threshold = 1.0 - u_float2 * 0.01;
float t0 = threshold - 0.15;
float t1 = threshold + 0.15;
vec2 texelSize = 1.0 / u_resolution;
float radius2 = radius * radius;
float sampleScale = clamp(radius * 0.75, 0.35, 1.0);
int samples = int(float(MAX_SAMPLES) * sampleScale);
float noise = hash(gl_FragCoord.xy);
float angleOffset = noise * GOLDEN_ANGLE;
float radiusJitter = 0.85 + noise * 0.3;
float ca = cos(GOLDEN_ANGLE);
float sa = sin(GOLDEN_ANGLE);
vec2 dir = vec2(cos(angleOffset), sin(angleOffset));
vec3 glow = vec3(0.0);
float totalWeight = 0.0;
// Center tap
float centerMask = smoothstep(t0, t1, dot(original.rgb, LUMA));
glow += original.rgb * centerMask * 2.0;
totalWeight += 2.0;
for (int i = 1; i < MAX_SAMPLES; i++) {
if (i >= samples) break;
float fi = float(i);
float dist = sqrt(fi / float(samples)) * radius * radiusJitter;
vec2 offset = dir * dist * texelSize;
vec3 c = texture(u_image0, v_texCoord + offset).rgb;
float mask = smoothstep(t0, t1, dot(c, LUMA));
float w = 1.0 - (dist * dist) / (radius2 * 1.5);
w = max(w, 0.0);
w *= w;
glow += c * mask * w;
totalWeight += w;
dir = vec2(
dir.x * ca - dir.y * sa,
dir.x * sa + dir.y * ca
);
}
glow *= intensity / max(totalWeight, 0.001);
if (u_int1 > 0) {
glow *= hexToRgb(u_int1);
}
vec3 result = blend(original.rgb, glow, u_int0);
result += (noise - 0.5) * (1.0 / 255.0);
fragColor = vec4(clamp(result, 0.0, 1.0), original.a);
}
+222
View File
@@ -0,0 +1,222 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform int u_int0; // Mode: 0=Master, 1=Reds, 2=Yellows, 3=Greens, 4=Cyans, 5=Blues, 6=Magentas, 7=Colorize
uniform int u_int1; // Color Space: 0=HSL, 1=HSB/HSV
uniform float u_float0; // Hue (-180 to 180)
uniform float u_float1; // Saturation (-100 to 100)
uniform float u_float2; // Lightness/Brightness (-100 to 100)
uniform float u_float3; // Overlap (0 to 100) - feathering between adjacent color ranges
in vec2 v_texCoord;
out vec4 fragColor;
// Color range modes
const int MODE_MASTER = 0;
const int MODE_RED = 1;
const int MODE_YELLOW = 2;
const int MODE_GREEN = 3;
const int MODE_CYAN = 4;
const int MODE_BLUE = 5;
const int MODE_MAGENTA = 6;
const int MODE_COLORIZE = 7;
// Color space modes
const int COLORSPACE_HSL = 0;
const int COLORSPACE_HSB = 1;
const float EPSILON = 0.0001;
//=============================================================================
// RGB <-> HSL Conversions
//=============================================================================
vec3 rgb2hsl(vec3 c) {
float maxC = max(max(c.r, c.g), c.b);
float minC = min(min(c.r, c.g), c.b);
float delta = maxC - minC;
float h = 0.0;
float s = 0.0;
float l = (maxC + minC) * 0.5;
if (delta > EPSILON) {
s = l < 0.5
? delta / (maxC + minC)
: delta / (2.0 - maxC - minC);
if (maxC == c.r) {
h = (c.g - c.b) / delta + (c.g < c.b ? 6.0 : 0.0);
} else if (maxC == c.g) {
h = (c.b - c.r) / delta + 2.0;
} else {
h = (c.r - c.g) / delta + 4.0;
}
h /= 6.0;
}
return vec3(h, s, l);
}
float hue2rgb(float p, float q, float t) {
t = fract(t);
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
if (t < 0.5) return q;
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
return p;
}
vec3 hsl2rgb(vec3 hsl) {
if (hsl.y < EPSILON) return vec3(hsl.z);
float q = hsl.z < 0.5
? hsl.z * (1.0 + hsl.y)
: hsl.z + hsl.y - hsl.z * hsl.y;
float p = 2.0 * hsl.z - q;
return vec3(
hue2rgb(p, q, hsl.x + 1.0/3.0),
hue2rgb(p, q, hsl.x),
hue2rgb(p, q, hsl.x - 1.0/3.0)
);
}
vec3 rgb2hsb(vec3 c) {
float maxC = max(max(c.r, c.g), c.b);
float minC = min(min(c.r, c.g), c.b);
float delta = maxC - minC;
float h = 0.0;
float s = (maxC > EPSILON) ? delta / maxC : 0.0;
float b = maxC;
if (delta > EPSILON) {
if (maxC == c.r) {
h = (c.g - c.b) / delta + (c.g < c.b ? 6.0 : 0.0);
} else if (maxC == c.g) {
h = (c.b - c.r) / delta + 2.0;
} else {
h = (c.r - c.g) / delta + 4.0;
}
h /= 6.0;
}
return vec3(h, s, b);
}
vec3 hsb2rgb(vec3 hsb) {
vec3 rgb = clamp(abs(mod(hsb.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return hsb.z * mix(vec3(1.0), rgb, hsb.y);
}
//=============================================================================
// Color Range Weight Calculation
//=============================================================================
float hueDistance(float a, float b) {
float d = abs(a - b);
return min(d, 1.0 - d);
}
float getHueWeight(float hue, float center, float overlap) {
float baseWidth = 1.0 / 6.0;
float feather = baseWidth * overlap;
float d = hueDistance(hue, center);
float inner = baseWidth * 0.5;
float outer = inner + feather;
return 1.0 - smoothstep(inner, outer, d);
}
float getModeWeight(float hue, int mode, float overlap) {
if (mode == MODE_MASTER || mode == MODE_COLORIZE) return 1.0;
if (mode == MODE_RED) {
return max(
getHueWeight(hue, 0.0, overlap),
getHueWeight(hue, 1.0, overlap)
);
}
float center = float(mode - 1) / 6.0;
return getHueWeight(hue, center, overlap);
}
//=============================================================================
// Adjustment Functions
//=============================================================================
float adjustLightness(float l, float amount) {
return amount > 0.0
? l + (1.0 - l) * amount
: l + l * amount;
}
float adjustBrightness(float b, float amount) {
return clamp(b + amount, 0.0, 1.0);
}
float adjustSaturation(float s, float amount) {
return amount > 0.0
? s + (1.0 - s) * amount
: s + s * amount;
}
vec3 colorize(vec3 rgb, float hue, float sat, float light) {
float lum = dot(rgb, vec3(0.299, 0.587, 0.114));
float l = adjustLightness(lum, light);
vec3 hsl = vec3(fract(hue), clamp(sat, 0.0, 1.0), clamp(l, 0.0, 1.0));
return hsl2rgb(hsl);
}
//=============================================================================
// Main
//=============================================================================
void main() {
vec4 original = texture(u_image0, v_texCoord);
float hueShift = u_float0 / 360.0; // -180..180 -> -0.5..0.5
float satAmount = u_float1 / 100.0; // -100..100 -> -1..1
float lightAmount= u_float2 / 100.0; // -100..100 -> -1..1
float overlap = u_float3 / 100.0; // 0..100 -> 0..1
vec3 result;
if (u_int0 == MODE_COLORIZE) {
result = colorize(original.rgb, hueShift, satAmount, lightAmount);
fragColor = vec4(result, original.a);
return;
}
vec3 hsx = (u_int1 == COLORSPACE_HSL)
? rgb2hsl(original.rgb)
: rgb2hsb(original.rgb);
float weight = getModeWeight(hsx.x, u_int0, overlap);
if (u_int0 != MODE_MASTER && hsx.y < EPSILON) {
weight = 0.0;
}
if (weight > EPSILON) {
float h = fract(hsx.x + hueShift * weight);
float s = clamp(adjustSaturation(hsx.y, satAmount * weight), 0.0, 1.0);
float v = (u_int1 == COLORSPACE_HSL)
? clamp(adjustLightness(hsx.z, lightAmount * weight), 0.0, 1.0)
: clamp(adjustBrightness(hsx.z, lightAmount * weight), 0.0, 1.0);
vec3 adjusted = vec3(h, s, v);
result = (u_int1 == COLORSPACE_HSL)
? hsl2rgb(adjusted)
: hsb2rgb(adjusted);
} else {
result = original.rgb;
}
fragColor = vec4(result, original.a);
}
+111
View File
@@ -0,0 +1,111 @@
#version 300 es
#pragma passes 2
precision highp float;
// Blur type constants
const int BLUR_GAUSSIAN = 0;
const int BLUR_BOX = 1;
const int BLUR_RADIAL = 2;
// Radial blur config
const int RADIAL_SAMPLES = 12;
const float RADIAL_STRENGTH = 0.0003;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform int u_int0; // Blur type (BLUR_GAUSSIAN, BLUR_BOX, BLUR_RADIAL)
uniform float u_float0; // Blur radius/amount
uniform int u_pass; // Pass index (0 = horizontal, 1 = vertical)
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
float gaussian(float x, float sigma) {
return exp(-(x * x) / (2.0 * sigma * sigma));
}
void main() {
vec2 texelSize = 1.0 / u_resolution;
float radius = max(u_float0, 0.0);
// Radial (angular) blur - single pass, doesn't use separable
if (u_int0 == BLUR_RADIAL) {
// Only execute on first pass
if (u_pass > 0) {
fragColor0 = texture(u_image0, v_texCoord);
return;
}
vec2 center = vec2(0.5);
vec2 dir = v_texCoord - center;
float dist = length(dir);
if (dist < 1e-4) {
fragColor0 = texture(u_image0, v_texCoord);
return;
}
vec4 sum = vec4(0.0);
float totalWeight = 0.0;
float angleStep = radius * RADIAL_STRENGTH;
dir /= dist;
float cosStep = cos(angleStep);
float sinStep = sin(angleStep);
float negAngle = -float(RADIAL_SAMPLES) * angleStep;
vec2 rotDir = vec2(
dir.x * cos(negAngle) - dir.y * sin(negAngle),
dir.x * sin(negAngle) + dir.y * cos(negAngle)
);
for (int i = -RADIAL_SAMPLES; i <= RADIAL_SAMPLES; i++) {
vec2 uv = center + rotDir * dist;
float w = 1.0 - abs(float(i)) / float(RADIAL_SAMPLES);
sum += texture(u_image0, uv) * w;
totalWeight += w;
rotDir = vec2(
rotDir.x * cosStep - rotDir.y * sinStep,
rotDir.x * sinStep + rotDir.y * cosStep
);
}
fragColor0 = sum / max(totalWeight, 0.001);
return;
}
// Separable Gaussian / Box blur
int samples = int(ceil(radius));
if (samples == 0) {
fragColor0 = texture(u_image0, v_texCoord);
return;
}
// Direction: pass 0 = horizontal, pass 1 = vertical
vec2 dir = (u_pass == 0) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 color = vec4(0.0);
float totalWeight = 0.0;
float sigma = radius / 2.0;
for (int i = -samples; i <= samples; i++) {
vec2 offset = dir * float(i) * texelSize;
vec4 sample_color = texture(u_image0, v_texCoord + offset);
float weight;
if (u_int0 == BLUR_GAUSSIAN) {
weight = gaussian(float(i), sigma);
} else {
// BLUR_BOX
weight = 1.0;
}
color += sample_color * weight;
totalWeight += weight;
}
fragColor0 = color / totalWeight;
}
+19
View File
@@ -0,0 +1,19 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
layout(location = 1) out vec4 fragColor1;
layout(location = 2) out vec4 fragColor2;
layout(location = 3) out vec4 fragColor3;
void main() {
vec4 color = texture(u_image0, v_texCoord);
// Output each channel as grayscale to separate render targets
fragColor0 = vec4(vec3(color.r), 1.0); // Red channel
fragColor1 = vec4(vec3(color.g), 1.0); // Green channel
fragColor2 = vec4(vec3(color.b), 1.0); // Blue channel
fragColor3 = vec4(vec3(color.a), 1.0); // Alpha channel
}
+71
View File
@@ -0,0 +1,71 @@
#version 300 es
precision highp float;
// Levels Adjustment
// u_int0: channel (0=RGB, 1=R, 2=G, 3=B) default: 0
// u_float0: input black (0-255) default: 0
// u_float1: input white (0-255) default: 255
// u_float2: gamma (0.01-9.99) default: 1.0
// u_float3: output black (0-255) default: 0
// u_float4: output white (0-255) default: 255
uniform sampler2D u_image0;
uniform int u_int0;
uniform float u_float0;
uniform float u_float1;
uniform float u_float2;
uniform float u_float3;
uniform float u_float4;
in vec2 v_texCoord;
out vec4 fragColor;
vec3 applyLevels(vec3 color, float inBlack, float inWhite, float gamma, float outBlack, float outWhite) {
float inRange = max(inWhite - inBlack, 0.0001);
vec3 result = clamp((color - inBlack) / inRange, 0.0, 1.0);
result = pow(result, vec3(1.0 / gamma));
result = mix(vec3(outBlack), vec3(outWhite), result);
return result;
}
float applySingleChannel(float value, float inBlack, float inWhite, float gamma, float outBlack, float outWhite) {
float inRange = max(inWhite - inBlack, 0.0001);
float result = clamp((value - inBlack) / inRange, 0.0, 1.0);
result = pow(result, 1.0 / gamma);
result = mix(outBlack, outWhite, result);
return result;
}
void main() {
vec4 texColor = texture(u_image0, v_texCoord);
vec3 color = texColor.rgb;
float inBlack = u_float0 / 255.0;
float inWhite = u_float1 / 255.0;
float gamma = u_float2;
float outBlack = u_float3 / 255.0;
float outWhite = u_float4 / 255.0;
vec3 result;
if (u_int0 == 0) {
result = applyLevels(color, inBlack, inWhite, gamma, outBlack, outWhite);
}
else if (u_int0 == 1) {
result = color;
result.r = applySingleChannel(color.r, inBlack, inWhite, gamma, outBlack, outWhite);
}
else if (u_int0 == 2) {
result = color;
result.g = applySingleChannel(color.g, inBlack, inWhite, gamma, outBlack, outWhite);
}
else if (u_int0 == 3) {
result = color;
result.b = applySingleChannel(color.b, inBlack, inWhite, gamma, outBlack, outWhite);
}
else {
result = color;
}
fragColor = vec4(result, texColor.a);
}
+28
View File
@@ -0,0 +1,28 @@
# GLSL Shader Sources
This folder contains the GLSL fragment shaders extracted from blueprint JSON files for easier editing and version control.
## File Naming Convention
`{Blueprint_Name}_{node_id}.frag`
- **Blueprint_Name**: The JSON filename with spaces/special chars replaced by underscores
- **node_id**: The GLSLShader node ID within the subgraph
## Usage
```bash
# Extract shaders from blueprint JSONs to this folder
python update_blueprints.py extract
# Patch edited shaders back into blueprint JSONs
python update_blueprints.py patch
```
## Workflow
1. Run `extract` to pull current shaders from JSONs
2. Edit `.frag` files
3. Run `patch` to update the blueprint JSONs
4. Test
5. Commit both `.frag` files and updated JSONs
+28
View File
@@ -0,0 +1,28 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform float u_float0; // strength [0.0 2.0] typical: 0.31.0
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
void main() {
vec2 texel = 1.0 / u_resolution;
// Sample center and neighbors
vec4 center = texture(u_image0, v_texCoord);
vec4 top = texture(u_image0, v_texCoord + vec2( 0.0, -texel.y));
vec4 bottom = texture(u_image0, v_texCoord + vec2( 0.0, texel.y));
vec4 left = texture(u_image0, v_texCoord + vec2(-texel.x, 0.0));
vec4 right = texture(u_image0, v_texCoord + vec2( texel.x, 0.0));
// Edge enhancement (Laplacian)
vec4 edges = center * 4.0 - top - bottom - left - right;
// Add edges back scaled by strength
vec4 sharpened = center + edges * u_float0;
fragColor0 = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);
}
+61
View File
@@ -0,0 +1,61 @@
#version 300 es
precision highp float;
uniform sampler2D u_image0;
uniform vec2 u_resolution;
uniform float u_float0; // amount [0.0 - 3.0] typical: 0.5-1.5
uniform float u_float1; // radius [0.5 - 10.0] blur radius in pixels
uniform float u_float2; // threshold [0.0 - 0.1] min difference to sharpen
in vec2 v_texCoord;
layout(location = 0) out vec4 fragColor0;
float gaussian(float x, float sigma) {
return exp(-(x * x) / (2.0 * sigma * sigma));
}
float getLuminance(vec3 color) {
return dot(color, vec3(0.2126, 0.7152, 0.0722));
}
void main() {
vec2 texel = 1.0 / u_resolution;
float radius = max(u_float1, 0.5);
float amount = u_float0;
float threshold = u_float2;
vec4 original = texture(u_image0, v_texCoord);
// Gaussian blur for the "unsharp" mask
int samples = int(ceil(radius));
float sigma = radius / 2.0;
vec4 blurred = vec4(0.0);
float totalWeight = 0.0;
for (int x = -samples; x <= samples; x++) {
for (int y = -samples; y <= samples; y++) {
vec2 offset = vec2(float(x), float(y)) * texel;
vec4 sample_color = texture(u_image0, v_texCoord + offset);
float dist = length(vec2(float(x), float(y)));
float weight = gaussian(dist, sigma);
blurred += sample_color * weight;
totalWeight += weight;
}
}
blurred /= totalWeight;
// Unsharp mask = original - blurred
vec3 mask = original.rgb - blurred.rgb;
// Luminance-based threshold with smooth falloff
float lumaDelta = abs(getLuminance(original.rgb) - getLuminance(blurred.rgb));
float thresholdScale = smoothstep(0.0, threshold, lumaDelta);
mask *= thresholdScale;
// Sharpen: original + mask * amount
vec3 sharpened = original.rgb + mask * amount;
fragColor0 = vec4(clamp(sharpened, 0.0, 1.0), original.a);
}
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Shader Blueprint Updater
Syncs GLSL shader files between this folder and blueprint JSON files.
File naming convention:
{Blueprint Name}_{node_id}.frag
Usage:
python update_blueprints.py extract # Extract shaders from JSONs to here
python update_blueprints.py patch # Patch shaders back into JSONs
python update_blueprints.py # Same as patch (default)
"""
import json
import logging
import sys
import re
from pathlib import Path
logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger(__name__)
GLSL_DIR = Path(__file__).parent
BLUEPRINTS_DIR = GLSL_DIR.parent
def get_blueprint_files():
"""Get all blueprint JSON files."""
return sorted(BLUEPRINTS_DIR.glob("*.json"))
def sanitize_filename(name):
"""Convert blueprint name to safe filename."""
return re.sub(r'[^\w\-]', '_', name)
def extract_shaders():
"""Extract all shaders from blueprint JSONs to this folder."""
extracted = 0
for json_path in get_blueprint_files():
blueprint_name = json_path.stem
try:
with open(json_path, 'r') as f:
data = json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning("Skipping %s: %s", json_path.name, e)
continue
# Find GLSLShader nodes in subgraphs
for subgraph in data.get('definitions', {}).get('subgraphs', []):
for node in subgraph.get('nodes', []):
if node.get('type') == 'GLSLShader':
node_id = node.get('id')
widgets = node.get('widgets_values', [])
# Find shader code (first string that looks like GLSL)
for widget in widgets:
if isinstance(widget, str) and widget.startswith('#version'):
safe_name = sanitize_filename(blueprint_name)
frag_name = f"{safe_name}_{node_id}.frag"
frag_path = GLSL_DIR / frag_name
with open(frag_path, 'w') as f:
f.write(widget)
logger.info(" Extracted: %s", frag_name)
extracted += 1
break
logger.info("\nExtracted %d shader(s)", extracted)
def patch_shaders():
"""Patch shaders from this folder back into blueprint JSONs."""
# Build lookup: blueprint_name -> [(node_id, shader_code), ...]
shader_updates = {}
for frag_path in sorted(GLSL_DIR.glob("*.frag")):
# Parse filename: {blueprint_name}_{node_id}.frag
parts = frag_path.stem.rsplit('_', 1)
if len(parts) != 2:
logger.warning("Skipping %s: invalid filename format", frag_path.name)
continue
blueprint_name, node_id_str = parts
try:
node_id = int(node_id_str)
except ValueError:
logger.warning("Skipping %s: invalid node_id", frag_path.name)
continue
with open(frag_path, 'r') as f:
shader_code = f.read()
if blueprint_name not in shader_updates:
shader_updates[blueprint_name] = []
shader_updates[blueprint_name].append((node_id, shader_code))
# Apply updates to JSON files
patched = 0
for json_path in get_blueprint_files():
blueprint_name = sanitize_filename(json_path.stem)
if blueprint_name not in shader_updates:
continue
try:
with open(json_path, 'r') as f:
data = json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.error("Error reading %s: %s", json_path.name, e)
continue
modified = False
for node_id, shader_code in shader_updates[blueprint_name]:
# Find the node and update
for subgraph in data.get('definitions', {}).get('subgraphs', []):
for node in subgraph.get('nodes', []):
if node.get('id') == node_id and node.get('type') == 'GLSLShader':
widgets = node.get('widgets_values', [])
if len(widgets) > 0 and widgets[0] != shader_code:
widgets[0] = shader_code
modified = True
logger.info(" Patched: %s (node %d)", json_path.name, node_id)
patched += 1
if modified:
with open(json_path, 'w') as f:
json.dump(data, f)
if patched == 0:
logger.info("No changes to apply.")
else:
logger.info("\nPatched %d shader(s)", patched)
def main():
if len(sys.argv) < 2:
command = "patch"
else:
command = sys.argv[1].lower()
if command == "extract":
logger.info("Extracting shaders from blueprints...")
extract_shaders()
elif command in ("patch", "update", "apply"):
logger.info("Patching shaders into blueprints...")
patch_shaders()
else:
logger.info(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"revision": 0, "last_node_id": 29, "last_link_id": 0, "nodes": [{"id": 29, "type": "4c9d6ea4-b912-40e5-8766-6793a9758c53", "pos": [1970, -230], "size": [180, 86], "flags": {}, "order": 5, "mode": 0, "inputs": [{"label": "image", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": null}], "outputs": [{"label": "R", "localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": []}, {"label": "G", "localized_name": "IMAGE1", "name": "IMAGE1", "type": "IMAGE", "links": []}, {"label": "B", "localized_name": "IMAGE2", "name": "IMAGE2", "type": "IMAGE", "links": []}, {"label": "A", "localized_name": "IMAGE3", "name": "IMAGE3", "type": "IMAGE", "links": []}], "title": "Image Channels", "properties": {"proxyWidgets": []}, "widgets_values": []}], "links": [], "version": 0.4, "definitions": {"subgraphs": [{"id": "4c9d6ea4-b912-40e5-8766-6793a9758c53", "version": 1, "state": {"lastGroupId": 0, "lastNodeId": 28, "lastLinkId": 39, "lastRerouteId": 0}, "revision": 0, "config": {}, "name": "Image Channels", "inputNode": {"id": -10, "bounding": [1820, -185, 120, 60]}, "outputNode": {"id": -20, "bounding": [2460, -215, 120, 120]}, "inputs": [{"id": "3522932b-2d86-4a1f-a02a-cb29f3a9d7fe", "name": "images.image0", "type": "IMAGE", "linkIds": [39], "localized_name": "images.image0", "label": "image", "pos": [1920, -165]}], "outputs": [{"id": "605cb9c3-b065-4d9b-81d2-3ec331889b2b", "name": "IMAGE0", "type": "IMAGE", "linkIds": [26], "localized_name": "IMAGE0", "label": "R", "pos": [2480, -195]}, {"id": "fb44a77e-0522-43e9-9527-82e7465b3596", "name": "IMAGE1", "type": "IMAGE", "linkIds": [27], "localized_name": "IMAGE1", "label": "G", "pos": [2480, -175]}, {"id": "81460ee6-0131-402a-874f-6bf3001fc4ff", "name": "IMAGE2", "type": "IMAGE", "linkIds": [28], "localized_name": "IMAGE2", "label": "B", "pos": [2480, -155]}, {"id": "ae690246-80d4-4951-b1d9-9306d8a77417", "name": "IMAGE3", "type": "IMAGE", "linkIds": [29], "localized_name": "IMAGE3", "label": "A", "pos": [2480, -135]}], "widgets": [], "nodes": [{"id": 23, "type": "GLSLShader", "pos": [2000, -330], "size": [400, 172], "flags": {}, "order": 0, "mode": 0, "inputs": [{"label": "image", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": 39}, {"localized_name": "fragment_shader", "name": "fragment_shader", "type": "STRING", "widget": {"name": "fragment_shader"}, "link": null}, {"localized_name": "size_mode", "name": "size_mode", "type": "COMFY_DYNAMICCOMBO_V3", "widget": {"name": "size_mode"}, "link": null}, {"label": "image1", "localized_name": "images.image1", "name": "images.image1", "shape": 7, "type": "IMAGE", "link": null}], "outputs": [{"label": "R", "localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": [26]}, {"label": "G", "localized_name": "IMAGE1", "name": "IMAGE1", "type": "IMAGE", "links": [27]}, {"label": "B", "localized_name": "IMAGE2", "name": "IMAGE2", "type": "IMAGE", "links": [28]}, {"label": "A", "localized_name": "IMAGE3", "name": "IMAGE3", "type": "IMAGE", "links": [29]}], "properties": {"Node name for S&R": "GLSLShader"}, "widgets_values": ["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\nlayout(location = 1) out vec4 fragColor1;\nlayout(location = 2) out vec4 fragColor2;\nlayout(location = 3) out vec4 fragColor3;\n\nvoid main() {\n vec4 color = texture(u_image0, v_texCoord);\n // Output each channel as grayscale to separate render targets\n fragColor0 = vec4(vec3(color.r), 1.0); // Red channel\n fragColor1 = vec4(vec3(color.g), 1.0); // Green channel\n fragColor2 = vec4(vec3(color.b), 1.0); // Blue channel\n fragColor3 = vec4(vec3(color.a), 1.0); // Alpha channel\n}\n", "from_input"]}], "groups": [], "links": [{"id": 39, "origin_id": -10, "origin_slot": 0, "target_id": 23, "target_slot": 0, "type": "IMAGE"}, {"id": 26, "origin_id": 23, "origin_slot": 0, "target_id": -20, "target_slot": 0, "type": "IMAGE"}, {"id": 27, "origin_id": 23, "origin_slot": 1, "target_id": -20, "target_slot": 1, "type": "IMAGE"}, {"id": 28, "origin_id": 23, "origin_slot": 2, "target_id": -20, "target_slot": 2, "type": "IMAGE"}, {"id": 29, "origin_id": 23, "origin_slot": 3, "target_id": -20, "target_slot": 3, "type": "IMAGE"}], "extra": {"workflowRendererVersion": "LG"}, "category": "Image Tools/Color adjust"}]}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More