Nodes/Helm/Helm Lab · Build axis from two recordings
ComfyUI Node

Helm Lab · Build axis from two recordings

22 KB, and it isn't a LoRA

By U-N-B-R-A-N-D-E-D·Created 25 days ago·Updated 10 days ago· 7
Helm Lab · Build axis from two recordings
    • axis
    ◄pos_tagpos►
    ◄neg_tagneg►
    ◄axisaxis►
    ◄safe_max0.00►

    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 (pos by 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 0 and 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.

    CategoryHelm/Lab

    Inputs (4)

    NameTypeDefaultDescription
    pos_tagSTRINGposName of the recording made WITH the idea.
    neg_tagSTRINGnegName of the recording made WITHOUT the idea.
    axisSTRINGaxisName of the axis file to write.
    safe_maxFLOAT0.000–10optional: the highest strength verified safe for this axis. 0 = unset. Stored with the axis and enforced by HelmApply.

    Outputs (1)

    NameTypeDescription
    axisSTRING—