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.
147 lines
7.5 KiB
Python
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",
|
|
}
|