Files
manzakin 16e9b41158 simpleshot: report the LoRA chain, and fix a guard that a LoRA would have broken
The node cannot ask the MODEL object which LoRAs it carries — they are baked into
its weights before it arrives — so the info line reads the model path out of the
workflow, the same way the checkpoint guard does. It names each active LoRA with
its strength and counts the switched-off slots, because a bypassed slot looks
almost identical to a live one on the canvas and 'why doesn't my character look
right' is usually exactly that.

Writing it exposed a real bug in the checkpoint guard: it walked ONE hop up the
model path, so with a LoRA loader in between it read the LORA's filename as the
checkpoint. On a correct graph that produced 'family unproven' at best and a
false MISMATCH REFUSAL at worst — a hard stop on a workflow with nothing wrong
with it. It now walks the whole chain and takes the loader at the far end.

138 offline checks, including the chain walk, a cycle that must not hang it, and
the ref2va refusal still firing THROUGH an active LoRA.
2026-08-27 15:00:36 +02:00

147 lines
7.5 KiB
Python

# -*- coding: utf-8 -*-
"""SimpleShot's first node: one image (or two, or none) to video with sound.
The whole MiniMax-H3 fl2va pipeline in a single node — conditioning, keyframes,
sampling and both decodes. Not because a monolith is elegant, but because the
graph it leaves behind is four loaders, this node, and a video saver: there is
almost nothing to wire, so there is almost nothing to wire wrongly.
What it does NOT offer is the point of the design: no sampler, no scheduler, no
cfg, no sigma shift, no negative prompt. Those are settings whose only realistic
use here is to break a working render. What it does offer is what changes the
picture: the prompt, up to two frames, how long, how big, how many steps.
The model behind it is a first/last-frame model, so identity comes from the
image you give it, not from reference photographs — a reference node is a
separate node, later, because a mode dropdown spanning two sampling paths is a
mistake this line has already paid for once.
"""
from . import _core
class SimpleShotH3FirstLast:
"""fl2va in one node: prompt (+ optional first/last frames) -> video + audio."""
CATEGORY = "SimpleShot"
FUNCTION = "render"
RETURN_TYPES = ("IMAGE", "AUDIO", "STRING")
RETURN_NAMES = ("images", "audio", "info")
OUTPUT_TOOLTIPS = (
"The frames. Wire into Create Video, then Save Video.",
"The generated soundtrack, already in step with the picture.",
"What actually ran: frame count, canvas, checkpoint, how the frames "
"were fitted. Wire it into a Preview Any node if you want to read it "
"on the canvas.",
)
DESCRIPTION = (
"MiniMax-H3 first/last-frame video WITH sound, in one node. Give it a "
"prompt and optionally a first frame, a last frame, or both. Nothing "
"here can be set to an illegal value: the menus only offer lengths and "
"canvases this model accepts, and the info output says what it used."
)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL", {"tooltip":
"The H3 fl2va checkpoint, from a Load Diffusion Model "
"node. Any loader works — swap in a smaller community "
"quant if 21 GB of weights is too much for your card."}),
"clip": ("CLIP", {"tooltip":
"The H3 text encoder, from a Load CLIP node."}),
"vae": ("VAE", {"tooltip":
"The VIDEO vae (minimax_h3_video_vae). Easy to swap with "
"the audio one by accident — this is the larger file."}),
"audio_vae": ("VAE", {"tooltip":
"The AUDIO vae (minimax_h3_audio_vae), the small one."}),
"prompt": ("STRING", {
"multiline": True, "dynamicPrompts": True,
"default": "",
"tooltip":
"Describe the shot: subject, what it does, the camera, "
"the light, and the sound you want. This model listens "
"to sound cues in the prompt — voices, footsteps, room "
"tone — because it renders picture and audio together."}),
"duration": (_core.DURATION_NAMES, {
"default": _core.DURATION_DEFAULT,
"tooltip":
"Clip length at 24 fps. Start on the 2 s quick test to "
"prove your install before spending a long render."}),
"resolution": (_core.RESOLUTION_NAMES, {
"default": _core.RESOLUTION_DEFAULT,
"tooltip":
"The 'match my image' entries take the SHAPE from your "
"first frame and only the SIZE from the menu, so your "
"picture is never stretched or cropped — use those "
"unless you have a reason not to. Smaller is much "
"faster: 'fast' is about 40% of the pixels of full "
"quality and 'draft' about 20%. The fixed sizes are for "
"text-only clips, where there is no image to match."}),
"steps": ("INT", {
"default": _core.STEPS_DEFAULT, "min": 1, "max": 100,
"tooltip":
"The one real cost/quality dial, and time scales almost "
"linearly with it. 14 is a good balance; the model's own "
"template uses 20, and this sampler converges early "
"enough that the difference is often hard to see. Try "
"8 on a draft canvas when you are only checking an "
"idea."}),
"seed": ("INT", {
"default": 0, "min": 0, "max": 0xffffffffffffffff,
"control_after_generate": True,
"tooltip":
"Changes the take. Left on randomize you get a fresh "
"one each time you press Run; set it to fixed to keep "
"a take you liked while you edit the prompt."}),
},
"optional": {
"first_frame": ("IMAGE", {"tooltip":
"Optional. The clip starts here. This is the geometry "
"anchor: it is STRETCHED to the canvas, so give it the "
"same shape as the resolution you picked or it distorts."}),
"last_frame": ("IMAGE", {"tooltip":
"Optional. The clip lands here. This one is CENTRE-CROPPED "
"instead of stretched, so a different shape loses its "
"edges rather than squashing."}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
def render(self, model, clip, vae, audio_vae, prompt, duration, resolution,
steps, seed, first_frame=None, last_frame=None,
unique_id=None, extra_pnginfo=None):
frames, width, height, snap_notes = _core.resolve_shape(
duration, resolution, first_frame, last_frame)
checkpoint = _core.checkpoint_note(model, extra_pnginfo, unique_id,
want="fl2va")
lines = ["%d frames at %d fps (%.2f s), canvas %dx%d, %d steps, seed %d"
% (frames, _core.FPS, frames / _core.FPS, width, height,
int(steps), int(seed)),
checkpoint,
_core.lora_note(extra_pnginfo, unique_id)]
lines += snap_notes
lines += _core.image_notes(first_frame, last_frame, width, height)
if not str(prompt).strip():
lines.append("the prompt is EMPTY — the model will invent "
"everything, including the sound")
info = _core.report("H3 first/last -> video + audio", lines)
conditioning, latent = _core.encode_fl2va(
clip, vae, prompt, width, height, frames,
first_frame=first_frame, last_frame=last_frame)
sampled = _core.sample_av(model, conditioning, latent, steps, seed)
images = _core.decode_video(vae, sampled)
audio = _core.decode_audio(audio_vae, sampled)
return (images, audio, info)
NODE_CLASS_MAPPINGS = {"SimpleShotH3FirstLast": SimpleShotH3FirstLast}
NODE_DISPLAY_NAME_MAPPINGS = {
"SimpleShotH3FirstLast": "SimpleShot H3 - First/Last Frame to Video + Audio",
}