Nodes/Helm/Helm Lab · Apply axis (manual strength)
ComfyUI Node

Helm Lab · Apply axis (manual strength)

Same prompt, same seed, one number changed

By U-N-B-R-A-N-D-E-D·Created 25 days ago·Updated 10 days ago· 7
Helm Lab · Apply axis (manual strength)
  • model
  • model
◄axisaxis►
◄block20►
◄strength0.10►
◄dial0.00►
◄cond_onlyfalse►
◄start_percent0.00►
◄end_percent1.00►
◄modepush►
◄mask►
◄layoutnone►
◄grid►
◄clamp_k1.0►
◄mask_offset0►

You want the room a bit emptier, the light a bit colder - so you add a word to the prompt and the whole frame changes. Composition, lens, the thing you liked. A prompt is a switch, not a dial. Helm Apply is the dial.

It sits between your model loader and your sampler, takes a MODEL and gives a MODEL back, and nudges the transformer's activations along a direction you've already recorded as a tiny .safetensors file. Weights are never touched, nothing is trained, and at strength = 0.0 it adds exactly zero.

How the injection actually works

An axis is just a vector: mean_pos - mean_neg, captured from two runs of the same model on two contrasting prompts. The shipped H3 density axis is 21,888 bytes - 22 KB is the whole artifact.

It clones your model patcher and hooks one transformer block. Every sampling step, after that block produces x, the hook returns x + strength * axis. The block list is discovered from the model itself (blocks, layers, transformer_blocks, double_blocks, and friends), and on architectures whose blocks return a tuple it touches only the visual stream - Flux returns (img, txt) and it knows that. On an undeclared tuple arch it raises a named error rather than guessing, which matters: guessing out[0] on SD3 or Qwen-Image would steer text conditioning instead.

Two things to know before you tune anything:

  • Axes are model-specific in scale, not meaning. The H3 axis has a norm around 3000–4000, the Z-Image one around 19. Strength values do not transfer. Not roughly, not at all.
  • Past the band it doesn't get stronger, it gets worse. Push far enough and the frame washes out - leaving the model's manifold, not a feature. Helm clamps for you if the axis recorded a safe_max.

The inputs you'll actually touch

Eight required fields; most of the time you'll set three.

  • model - from your loader.
  • axis - the axis file's name, no extension. Resolved from ComfyUI/user/steer/, then models/steer/, then the bundled example_axes/.
  • block - -1 means adopt the index recorded inside the axis file. That's the sane default: an axis built at block 20 wants to be applied at block 20.
  • strength - the scalar. Calibration table below.
  • dial - normalized 0–1. Above 0, and if the axis recorded a safe_max, strength becomes dial × safe_max. This is the one I'd hand a beginner.
  • cond_only - false preserves what the author measured; true applies only to conditional batch items, which its tooltip recommends for CFG models. The shipped axes were not measured with it on.
  • start_percent / end_percent - a sampling-fraction window. Narrow it to steer early and spare late detail.

Then the optional half. mode is push (published), norm (rescale each token to its original norm) or clamp (limit the projection move to clamp_k × ‖x‖). The author measured the alternatives and they lose badly: of 53 labelled axes, push holds 10, norm 1, clamp 2. Start in push. mask + layout + grid + mask_offset do regional injection - fiddly, and the README is refreshingly honest that a mask localises where the delta is added, not where the change appears, because attention spreads it. Skip all four until you're comfortable.

Output: one model socket. Wire it into your sampler (or into the next Helm Apply).

Quickstart that won't waste an evening

Use a model with a measured axis - H3 or Z-Image/Lumina2 - and start from the calibrated numbers:

| Model | Block | Strength band | Shipped safe_max | | --- | --- | ---: | --- | | MiniMax H3 | 20 (of 50) | 0.01 – 0.30 | 0.3 | | Z-Image / Lumina2 | 15 (of 30) | 0.50 – 3.00 | 0.25 on z_room_density |

Z-Image is the cheap way in: core nodes only, consumer card. The H3 workflows need the third-party MiniMaxH3Director pack plus a ~21 GB int8 UNet and a Qwen3-VL 32B encoder - not a 24 GB conversation.

Chaining composes: HelmApply (mood) → HelmApply (density) → sampler. All hooks live on one shared wrapper, so the second node doesn't overwrite the first.

Install

ComfyUI Manager → search Helm → install → restart. Or by hand:

cd ComfyUI/custom_nodes
git clone https://github.com/U-N-B-R-A-N-D-E-D/comfyui-helm.git comfyui-helm

Restart, then open Templates and search helm, or drag example_workflows/z_image_helm.json onto the canvas. Dependencies are light - safetensors and numpy on Python 3.10+, both already present, no requirements.txt to wreck anything. The example axes ship inside example_axes/ and resolve on a fresh clone.

Not Helm's fault, but: custom node packs don't run on Comfy Cloud. Local or self-hosted only.

Where people get burned

Dimension mismatch. axis 'density' dim 5376 != block output dim 3840 means you fed an H3 axis to Z-Image, or applied at the wrong module. Axes are per-model; fix the file.

Axis not found. A FileNotFoundError listing every directory searched: the axis field doesn't match a file. Usually an extension you added, or a typo that created a second axis upstream.

"My Z-Image image is mush at strength 1.0." That's the wash. The general Z-Image band tops out near 3.0, but the shipped room-density axis is clamped at 0.25 because its wash shows at 0.5. Different axes, different ceilings - confirm with a render or two instead of trusting the table.

You expected prompt-mixing. It isn't ConditioningAverage. From the same prompt, a mix does nothing until it abruptly swaps the scene; Helm grades it. Want the scene to flip? A mix is cheaper.

Easy to over-trust, so: only two models have been measured with this. Krea 2 templates are wired but not execution-validated, LTX-2.3 ships tested: false, Flux has no dose. On anything else, build your own axis and sweep - that's what Helm Collect and Helm Axis are for.

CategoryHelm/Lab

Inputs (14)

NameTypeDefaultDescription
modelMODELThe model to steer.
axisSTRINGaxisName of the axis file to apply.
blockINT20-1–512-1 = adopt the block recorded inside the axis file (the block it was built at); otherwise this explicit index
strengthFLOAT0.10-10–10scale of the injected axis. Model-specific; see calibration.json
dialFLOAT0.000–1optional normalized 0-1 dial. If >0 and the axis recorded a safe_max, strength = dial * safe_max (keeps you inside the verified band).
cond_onlyBOOLEANfalseDefault False preserves the published result. True (recommended for CFG models) adds the axis only to cond batch items; current axes were NOT measured with cond_only.
start_percentFLOAT0.000–1sampling-fraction window start. Default 0.0 preserves the published result.
end_percentFLOAT1.000–1sampling-fraction window end. Default 1.0 preserves the published result.
modeoptCOMBOpushpush = published x+strength*axis. norm = same, rescaled to the token's original norm. clamp = move the axis projection, clamped to +/- clamp_k*||x||.
maskoptSTRINGpath to a black/white PNG. White = apply the axis there (requires layout + grid). Empty = global (published behavior).
layoutoptCOMBOnonetoken order of the block output. h_w = image; t_h_w = frame-major video; h_w_t = frame-inner. Never guessed: the mask is refused if the grid does not multiply to the token count.
gridoptSTRINGexplicit H,W (image) or H,W,T (video). Required whenever a mask is given; the grid is never inferred.
clamp_koptFLOAT1.00.1–10clamp mode only: projection limit as a multiple of each token's norm.
mask_offsetoptINT00–1000000leading packed tokens to skip before the mask. Z-Image packs 32 text/cond rows before the 4096 spatial tokens; H3 packs [text|cond|audio|video], so pass the row count before the video slice. Requires mask.

Outputs (1)

NameTypeDescription
modelMODEL—