Helm Lab · Build axis from two recordings
22 KB, and it isn't a LoRA
- axis
Here's the comparison that makes this node click. A style LoRA is 10–200 MB, trained on a dataset you assembled, and needs a compatible checkpoint to not fall apart. A Helm axis is 22 KB, built from two prompts in a minute, and is mathematically a direction inside the model - mean_pos − mean_neg. No adapter weights, no rank, no alpha.
Helm Axis is where you build that file: two collected activation tags in, one self-describing .safetensors axis out. And check the output type before you wire anything - it's a STRING. Not a tensor, not a preview. The node hands you the name of the axis it wrote, and that name is what every other Helm node resolves from disk.
What it computes
Nothing clever, which is the point:
axis = mean_pos − mean_neg
Both tags must already exist as files written by Helm Collect, in ComfyUI/user/steer/, models/steer/ or example_axes/. The difference vector isn't normalised - its scale is whatever the model's activations happened to be, which is why strength values are so wildly model-specific. The H3 density axis has a norm in the thousands; Z-Image's room-density axis sits around 19.
Then it writes one safetensors file whose header carries what later nodes need: kind: axis, dim, block, model, module, norm, safe_max and a from record of the two tags. Two things follow: no pickle path, so a downloaded axis can't execute anything, and because the block index is baked in, Helm Apply's block = -1 adopts it for you.
The inputs
- pos_tag - the collection tag whose prompt you want more of (
posby default). - neg_tag - the contrast run (
neg). - axis - the filename to write, no extension. This is the name you'll type into Helm Apply. Reusing an existing name overwrites it without asking, which is either convenient or a way to lose a good axis.
- safe_max - the field people skip and shouldn't. Your verified ceiling: the highest strength you've actually rendered and been happy with. Leave it at
0and it's unset, so nothing clamps and the dial does nothing.
Output: axis, the name string. Type it into Helm Apply's axis field, or right-click that widget → Convert widget to input and wire it. The pack's helm_vector.json example sends it to a preview so you can read the name off the canvas.
Set safe_max. Actually set it.
This is the field that makes the pack safe to hand to someone else. Once safe_max is stored, two things happen in Helm Apply: dial becomes a real 0–1 control (strength = dial × safe_max), and a strength above your ceiling gets clamped with a console message instead of quietly frying the frame.
The shipped numbers show the shape of it - H3 axes record safe_max: 0.3, the Z-Image room-density axis records 0.25 because its wash shows at 0.5. Picking yours is a render job: apply the new axis at three or four values, find the last one that still reads as "the same scene, shifted", and write that in. Record the ceiling, not the value you liked best; the dial can always go lower.
Install
ComfyUI ComfyUI Manager → search Helm → install → restart, or:
cd ComfyUI/custom_nodes
git clone https://github.com/U-N-B-R-A-N-D-E-D/comfyui-helm.git comfyui-helm
Dependencies are safetensors and numpy on Python 3.10+, both already there. This node needs no model at all - it's file arithmetic - so producing axes is instant; producing the tags is the slow part, and that's Helm Collect's two renders.
Axes live in ComfyUI/user/steer/; reads check that, then models/steer/, then example_axes/. To share one, drop the 22 KB file in models/steer/ and it'll be found. Local or self-hosted only; Comfy Cloud won't take custom nodes.
Where people get burned
"An axis between different blocks is meaningless." Verbatim from the node, which compares the block and model metadata on your tags and refuses to subtract across them. Correct - and why you collect both passes at the same block.
The norm is tiny. The console prints [helm] axis '<name>' dim=… norm=… every time. Near zero means your two prompts produced essentially the same activations at that block and you've built a null axis. Change the contrast or the block; not the strength.
The axis does nothing at any strength. You're in the majority: the 78-axis Z-Image test found only 10 of 53 labelled directions held up. Most contrast pairs don't concentrate into one usable direction. Not a broken node - the model refusing to be that simple. Sharper, single-property contrasts are the lever.
You expected it to work on another model. Axes aren't portable. H3 is dim 5376, block 20 of 50; Z-Image is dim 3840, layer 15 of 30. Feed one to the other and Helm Apply raises a dim-mismatch error rather than doing something plausible-looking and wrong. That's a feature.
You're calling it a LoRA. It isn't, and the differences cut both ways: 22 KB and instant, but it's a direction, not a learned behaviour - no new concept, no character, no style the model didn't already know. Helm's own framing is the honest one: steer what it knows, train what it doesn't.
One expectation to calibrate: this pack is new and narrow. Measured results exist for H3 and Z-Image; Krea 2 is wired but unvalidated, LTX ships tested: false, Flux has no dose. On anything else you're the one doing the measurement - which is fine. That's what this node is for.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| pos_tag | STRING | pos | Name of the recording made WITH the idea. |
| neg_tag | STRING | neg | Name of the recording made WITHOUT the idea. |
| axis | STRING | axis | Name of the axis file to write. |
| safe_max | FLOAT | 0.000–10 | optional: the highest strength verified safe for this axis. 0 = unset. Stored with the axis and enforced by HelmApply. |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| axis | STRING | — |