comfyui-jz
Jacques' personal ComfyUI nodes (Gemini outpaint, padding, extras)
Nodes (24)
Stitch the Original Back Without the Seam
Kill the One-Pixel Fill-Color Rim Gemini Leaves Behind
Gemini image generation billed in real dollars
Make Your Canvas Match What Gemini Actually Returns
The before/after wipe, baked into frames instead of eyeballed in a viewer
A $0.62 video with no undo button
Rescue a paid-for video by its task id
Seedream on your own key, watermark actually off
The pick that survives a reordered list
The paste node that ends the size math
Stop squinting at raw strings
A trimap from two sliders
The long edge answer in two INTs
The branch that only runs when it has to
The bouncer your batch workflows are missing
Pick a Closed Image Model From a Dropdown, Right Inside Your Graph
A captioner that fails on purpose
The Resize And Pad your outpainting actually wants
Taming mixed-size batches
The Resolution Selector You Can Actually Wire Into a Graph
Resize without stretching the subject
The resolution shift ComfyUI forgot to give you
One line out of many, on a seed you control
Lazy if/else that mutes dead branches
comfyui-jz
personal comfyui nodes, in the jz/ category so they never mix with the installed packs + they are easy to find. 24 nodes.
when editing existing nodes, ONLY append widgets and outputs... so that old workflows still work. node keys are frozen too — a few still read Gemini* for that reason.
contents
- jz/api (6) : Gemini Generate · OpenRouter VLM · OpenRouter Image · BytePlus Seedream · BytePlus Seedance · BytePlus Seedance Fetch
- jz/image (12) : Composite · Composite Back · Double Threshold · Edge Sizes · Image Sanity · Pad Calculator · Resize And Pad · Resize Long Edge · Resolution Selector · Seam Carve · Seam Repair · Before/After Slider
- jz/sampling (1) : Shift Sigmas
- jz/util (5) : Choice · Display JSON · Fallback · String Picker · Switch
- layout : how the package is put together
layout
__init__.py auto-discovers every nodes/**/*.py that exports NODE_CLASS_MAPPINGS, so adding a node is dropping a file — nothing to register. Files starting with _ are skipped.
shared code lives in common/ (never auto-discovered):
| module | |
|---|---|
| secrets.py | resolve_key() — every provider key resolves the same way: node input → env var → .env → config.ini. openrouter_key() / byteplus_key() are four-line adapters |
| http.py | pooled session, status-aware retries (408/429/5xx + Retry-After), truncate_b64 for sane error messages |
| images.py | tensor ↔ PIL ↔ base64/data-URL, pils_to_batch, BT.601 luma, the shared INTERPOLATION list |
| nodes.py | node-authoring helpers: AnyType/ANY wildcard sockets, ComboAny, scalar() for INPUT_IS_LIST, SEPARATORS, format_usd |
| model_cache.py | model dropdowns served instantly from a 24h cache, refreshed on a background thread — INPUT_TYPES() never blocks on the network |
| openrouter.py / byteplus.py | the per-provider adapters over that cache, plus each API's request shape |
| gemini_dims.py | the aspect-ratio × resolution → exact-size table, shared by three nodes |
| fill_color.py | padding-colour search (edge-average, and a colour provably absent from the image) |
| google_auth.py | service account → OAuth2 token |
web/ holds the frontend extensions (jz_choice.js, jz_display_json.js).
nodes
jz/api
custom nodes using https calls, with retries on 429/5xx responses.
keys are resolved server-side and must never be stored in workflows. every node resolves the same way — the api_key widget (leave it empty!) → environment variable → .env at the pack root → config.ini. both files are gitignored:
| provider | env / .env | config.ini |
|---|---|---|
| openrouter | OPENROUTER_API_KEY | [API] OPENROUTER_API_KEY |
| byteplus | BYTEPLUS_API_KEY or ARK_API_KEY | [BYTEDANCE] ARK_API_KEY |
⚠️ byteplus keys are region-scoped. a key issued for ap-southeast returns 401 AuthenticationError on eu-west and vice versa — and byteplus words it as "the API key ... is missing or invalid", which reads like a key problem. make the region widget match the key. the node's 401 message now says this.
| gemini | SERVICE_ACCOUNT_BASE64 (base64 of the service-account json) | — |
- jz Gemini Generate, vertex or generativelanguage generateContent. it works with zero images (text-to-image), single images, batches or a proper image list. when using
batch_size, it fires parallel calls (shared token, with per-call retries). the outputs are the image plus a usage summary and total token count. api key is the base64 encoded service account, stored in the.envasSERVICE_ACCOUNT_BASE64=....aspect_ratioandresolutionare dropdowns, built from the same dimension table as jz Pad Calculator so they can't drift (minusauto, which is a pad-calculator fitting mode, not an api value). to drive them from a wire instead, use theaspect_ratio_in/resolution_insockets — connected beats the dropdown, and the value is checked against the api's list before a request is spent on it

- jz OpenRouter VLM, vision/text through openrouter. it raises on errors instead of passing them downstream. also downscales images before upload (
max_edge), and sends every frame of a batch as a separate image in one call (so "describe the two images" just works), and outputs the cost. thereasoningwidget defaults tolow(reasoning models otherwise burnmax_tokenson hidden thinking and return truncated answers). api key is stored in theconfig.ini, under the [API] router asOPENROUTER_API_KEY=...

- jz OpenRouter Image, image generation and editing through openrouter. a separate node from the VLM one because it's a different api, not a mode of it:
POST /api/v1/images(not/chat/completions), aprompt/n/aspect_ratio/resolutionbody, images back asdata[].b64_json, and its own model catalogue at/api/v1/images/modelsthat doesn't overlap/api/v1/models. wire an IMAGE in and every frame becomes aninput_referencesentry — that's how editing / img2img works here. outputs the IMAGE batch plus the cost and a usage json.autoon a widget omits that field entirely rather than sending a default: supported parameters vary sharply per model —resolutiondoesn't exist ongpt-5-imageorflux.2-pro,ncaps at 1 for most models but 10 forgpt-5-image,seedis unsupported ongemini-3-pro-image.
model dropdowns on the openrouter and byteplus nodes are the live catalogue, cached to a gitignored
models_cache.json(24h) and refreshed on a background thread —INPUT_TYPES()never blocks on the network, so a slow or unreachable provider can't stall comfyui startup or break node registration offline. curated favourites stay pinned at the top andcustomreaches anything not listed. byteplus is filtered bytask_type, notmodalities(which is incomplete upstream — a live model can have nooutput_modalitiesat all).
watch the cost: these models bill per output token, not per image. a 1024x1024 from gpt-5-image-mini (the cheapest) is ~4160 image tokens ≈ $0.033 — the per-token figure in openrouter's model listing looks tiny but multiplies fast, and the bigger models are ~15x that. the cost output reports what each call actually charged
-
jz BytePlus Seedream (image), the official byteplus modelark image api (
ark.ap-southeast.bytepluses.com/api/v3,eu-westtoo). synchronous, ~8s.1k/2k/4kor a custom WxH (921,600–16,777,216 px),nimages per call, and wiring an IMAGE in makes every frame a reference — that's how editing and multi-reference blending work. parameter support varies per model and is enforced (seedream-4-0rejectsoutput_format;dola-seedream-5-0-prorejects4k), so only what you actually set is sent. the api's watermark default is on, so the node always sends the flag explicitly. billing is per output token, exact inusage -
jz BytePlus Seedance (video), submits a generation task, polls it, and returns a native VIDEO — a 5s 1080p clip decoded to an IMAGE batch would be ~3 GB. also outputs
task_idandapplied. ⚠️ this is the node that validates hardest, and here is why. seedance parameters ride as text flags on the prompt (--rs --rt --dur --fps --wm --cf --seed), and the server validates only--resolutionand--duration. every other flag — and any unknown flag — is silently ignored and still billed: a typo'd--ratiobuys a perfectly valid, completely wrong video. worse, a running task cannot be cancelled (DELETE→409), so the charge is committed the moment the POST returns. so: flags are whitelisted client-side before anything is sent, the token cost is printed first,seedis deliberately notcontrol_after_generate(a re-queue would spend again), billable POSTs are sent once with no retries (a retried submit whose first attempt landed bills twice), andappliedechoes the parameters the server really used so a dropped flag is visible instead of silent. cost is per token: 1080p/16:9/5s/24fps = 246,840 tokens ≈ $0.62 onseedance-1-0-pro -
jz BytePlus Seedance Fetch (by task id), picks a job up by its
cgt-…id. reads are free and tasks live 48h, so a graph that errors after the spend is fully recoverable — and a job that outranpoll_timeoutis not money lost
jz/image
plain image ops (often image in image out), no API involved
- jz Composite Back, pastes the original back onto the generated image with feathered edges [outpainting workflow]

- jz Seam Repair, deterministically cleans leftover fill-color seams at the canvas edge [outpainting workflow]

- jz Pad Calculator, picks the best supported aspect/resolution (for Nano Banana Pro) for an image and computes the padding to get there [outpainting workflow]

- jz Resize Long Edge (list), normalizes a list or batch of mixed-size images to one long edge, outputs a list (of images).
interpolationpicks the resample method (same five as jz Resize And Pad); a frame already at the target size is passed through untouched, and alpha is preserved the appendedbatchoutput is the same frames as one tensor, for nodes that need a real batch rather than a list. a tensor can't hold mixed sizes, so each frame is centred on the smallest canvas that fits them all and the rest is padded opaque black — uniform inputs are padded not at all, the batch is just a stack. an RGBA beside an RGB levels the RGB up with an opaque alpha. note the two outputs are for different things: feedimages(the list) to anything that takes images one at a time, andbatchwhere a single tensor is required. for api reference images prefer the list — the byteplus nodes take it directly, and padding bars would otherwise be uploaded for the model to see

- jz Seam Carve, content-aware resize (Avidan-Shamir seam carving + forward energy, arXiv:2608.04329). carve or enlarge either dimension, protect/remove regions with MASK inputs. numba-compiled fast path when numba is installed, multi-frame batches carve in parallel. note: forward energy algorithm will cut through flat uniform regions, protect the product with a mask when it matters!

- jz Edge Sizes, outputs the long and short edge of an image as INTs (width/height sorted, orientation-agnostic)

- jz Composite, pastes a source image onto a destination at a named anchor (center, corners, edge midpoints) with x/y offset and a border margin. optional MASK blends the source through it (a 4-channel source blends through its own alpha). no scaling — resize upstream. raises if the source doesn't fit

- jz Double Threshold, two-sided binarization: luma above
highgoes white, belowlowgoes black, the band in between goes transparent. outputs the RGBA image (feeds jz Composite's alpha blending directly), a trimap MASK (1/0.5/0) and the decided-pixels alpha MASK

-
jz Image Sanity, flags degenerate frames — empty (0 px, always checked), flat, too dark, too bright, fully transparent — each check toggleable with its own threshold. it never raises on a bad frame: it measures and reports, so you branch on
okwith jz Switch / jz Fallback and decide yourself whether that means retry, substitute or skip. flatness is measured per channel (a flat red frame has channel stds of 0 but a whole-tensor std of ~0.47, so a single global std would call it textured). takes a batch or an image list, and the per-frame outputs (image,ok,reason,std,mean) are lists — one verdict per frame;report(json, feeds jz Display JSON) andall_okare scalars for whole-batch decisions -
jz Resize And Pad, comfyui's own Resize And Pad Image with the padding colour set free — it only offers white or black. same fit-and-centre (
min(tw/w, th/h), always scale to fit), butpadding_colortakes#rrggbb/#rgb/black/white, or wire a solid-colour image intocolor_image(jz Pad Calculator'sfill_color) and it's sampled from there. also outputs the padding region as a MASK (1 = bar, 0 = image), and channels are preserved — an RGBA input stays RGBA with the padding opaque. note:interpolationdefaults tolanczos, which round-trips through 8-bit in comfy's implementation; pickareaorbicubicto stay in float. a frame that already matches the target is passed through untouched rather than resampled -
jz Resolution Selector, aspect ratio → width/height. comfyui's core Resolution Selector labels its options
16:9 (Widescreen)and takes a COMBO, which no STRING output can connect to — so it can't be driven from jz Pad Calculator. this one uses plain ratios, andaspect_ratio_inis a STRING socket you can actually wire (connected beats the dropdown; it also accepts the core node's parenthesised form).modepicks how the size is computed:tablereturns the exact dimensions gemini emits (straight from the same DIMENSION_MAP as jz Pad Calculator, 10 ratios — core has 8, this adds4:5and5:4),megapixelsuses core's own formula for any ratio and any target. they differ slightly on purpose: 16:9 at 1 MP is1368x768by the formula but1376x768in the table -
jz Before/After Slider, animates a wipe between two images: a divider sweeps across revealing
afteroverbefore, holds, sweeps back — so the batch loops seamlessly. outputs the frames as an IMAGE batch (plusframe_countandfps), so you pick the encoder:VHS_VideoCombinefor a gif, or core'sSaveAnimatedWEBP/SaveAnimatedPNG/SaveWEBM. frames stay editable, so you can composite a caption on them first. timing is two numbers —sweep_secondsandhold_seconds(the start-end hold is split across the loop seam, so a looping player dwells equally at both ends).scaleresizes both inputs first: it's the lever on output size, and the handle scales with it (1.0skips resampling entirely).orientationswitches to a horizontal divider. mismatched input sizes raise — match them with jz Resize And Pad
jz/sampling
- jz Shift Sigmas (flow match), applies the resolution-dependent shift that flow-match models use, to a SIGMAS tensor:
mu = base_shift + (max_shift - base_shift) * (tokens - min_tok) / (max_tok - min_tok), thensigma = e^mu / (e^mu + 1/t - 1), withtokens = (W/16) * (H/16). comfyui has both halves but never the combination —ManualSigmasemits explicit sigmas with no shift, andModelSamplingFluxcomputes the identical mu (and the identical token count) but patches the MODEL, not a SIGMAS output. so there's no built-in way to take a schedule and shift it; this is that missing step. one node covers the families, which differ only in constants: fluxmax_shift 1.15, max_tokens 4096, qwen-imagemax_shift 0.9, max_tokens 8192. wireManualSigmas-> this ->SamplerCustom; with1.0, 0.9375, 0.875, 0.75, 0.5, 0.25it reproduces qwen-image viggle-turbo exactly. resolution comes from thewidth/heightwidgets, or from a connected LATENT (which uses the latent's owndownscale_ratio_spacial). themuandtokensoutputs are there because a wrong token count is otherwise silently wrong.append_zeroadds the terminal 0 samplers need, sinceManualSigmasdoesn't
jz/util
- jz String Picker, picks one string from a list (one per line or custom separator), random (seeded) or by wrapping index

- jz Fallback (lazy if/else), passes
primaryif present, otherwise evaluatesfallback. the unused branch never executes

- jz Switch (lazy if/else), boolean-driven: outputs
on_trueoron_falsedepending oncondition, the other branch never executes. both branches are optional: if the selected one is not connected, downstream nodes are silently skipped (if true output the image, else output nothing)

- jz Display JSON, takes a string of json and renders it in the node as a collapsible syntax-highlighted tree (copy button included). invalid json shows a parse-error banner + the raw text instead of killing the run. the view survives save/reload, and the prettified string passes through as output

- jz Choice, picks from a list of choices (STRING input, one per line or another separator) by NAME — reordering the list upstream never silently changes the pick, it either still matches or raises listing the options. when the choices come from a string-literal node, the
choicewidget turns into a real dropdown (live-refreshed); with runtime-computed choices it stays a text field. outputs the value and its index

screenshots
the older nodes have one; these don't yet — drop a screenshots/<name>.png in and add an !\[name\](screenshots/name.png) line:
jz_openrouter_image · jz_byteplus_seedream · jz_byteplus_seedance · jz_byteplus_seedance_fetch · jz_image_sanity · jz_resize_and_pad · jz_resolution_selector · jz_before_after_slider · jz_shift_sigmas