comfyui-jz
Jacques' personal ComfyUI nodes (Gemini outpaint, padding, extras)
Nodes (28)
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
1,500 fal models inside your own graph, checked before they bill you
The branch that only runs when it has to
Kling and Hailuo clips in your graph — where the JSON check earns its keep
The bouncer your batch workflows are missing
Gpt-image edits in your own graph, billed to your own OpenAI key
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
When Your Pose Skeleton Renders the Character Too Thin
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. 27 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 (9) : Gemini Generate · OpenRouter VLM · OpenRouter Image · BytePlus Seedream · BytePlus Seedance · BytePlus Seedance Fetch · fal Image · fal Video · OpenAI Image
- 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 / fal.py / openai.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 |
| fal.ai | FAL_KEY or FAL_API_KEY | [FAL] api_key |
| openai | OPENAI_API_KEY | [OPENAI] OPENAI_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 fal Image and jz fal Video, fal.ai through its queue api (
queue.fal.run,Authorization: Key …). key fromFAL_KEYin.envor[FAL] api_keyinconfig.ini. nofal-clientdependency — the rest is small enough for the pack's own http layer. fal has ~1500 models and every one takes different arguments, so model-specific parameters go in aparamsjson widget rather than a fixed widget set. what makes that safe: fal publishes each model's openapi schema, free and unauthenticated, so the node validates against the real schema before anything billable is sent — a typo'd key, a bad enum, a wrong type or a missing required field raises locally and lists what's allowed. the console also prints the model's accepted parameters so you don't have to go look them up. the schema also decides how a wired IMAGE is sent: models declaringimage_urlsget every frame,image_urlgets the first, and a text-to-image model that takes no image at all says so instead of being sent an argument it would reject. both nodes take a batch or a LIST, so one call either way. fal can cancel a running job, so a request that outrunspoll_timeoutis cancelled rather than left billing. billable submits are sent once with no retries. video returns a native VIDEO plus therequest_id -
jz OpenAI Image,
gpt-imagegeneration and editing on the official openai api (api.openai.com/v1). nothing wired =/images/generations; wire an IMAGE (batch or LIST, up to 16 frames) and it's/images/editsin its json form, every frame one reference. an optional MASK applies to the first image (white = area to change) and is converted to the alpha png the api wants, resized to that image.autoon a widget omits the field: support varies per model:xhigh/maxquality exist only ongpt-image-2.5-*, sizes beyond the three classic ones andcustomneedgpt-image-2+ (custom WxH is checked locally: multiples of 16, ratio within 3:1, edge ≤3840, 655,360–8,294,400 px), andinput_fidelityis rejected bygpt-image-2+.background: transparentputs the alpha on themaskoutput. billed per token;costis computed fromusagewith openai's published rates. measured: a 1024² low-quality edit ongpt-image-2.5-flare= 1024 image-in + 34 text-in + 196 image-out tokens ≈ $0.014. there is no idempotency key, so the billable POST is sent once, no retries. no openai video node: openai shut down sora and the whole/v1/videosapi on 2026-09-24, with no replacement
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 · jz_fal_image · jz_fal_video