feat: MiniMax H3 Reference to Video works text-only; with no picture a plain stand-in of the Width/Height shape feeds the size chain (found on a real GPU)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-15 15:03:52 +03:00
co-authored by Claude Sonnet 5.5
parent fd3224d9a4
commit 1fc5d99dcc
6 changed files with 46 additions and 4 deletions
+7
View File
@@ -18,6 +18,13 @@ export async function buildInputs({ project, scene, slots, expand, frames, hooks
for (const slot of (slots || []).filter((s) => s.node === "FunPackShotCamera")) put(slot, "project", project.id || ""); // what the node keeps per project (the words focus infusion draws on), also for pipelines saved before it was asked for
const source = rolesAt(slots, "assets.source_image")[0];
if (source) put(source.slot, "media_id", project.generation_mode === "t2v" ? "" : scene.source_image || ""); // "from a prompt" starts every shot without a picture
const like = source && source.role.blank_like;
if (like && !raw[source.slot.id].media_id) { // text-only: a plain picture of the size the pipeline asks for stands in
const target = (slots || []).find((s) => s.id === like.slot) || { inputs: {} };
const size = (name) => (raw[target.id] || {})[name] ?? (target.inputs || {})[name];
put(source.slot, "blank_width", Number(size(like.width)) || 0);
put(source.slot, "blank_height", Number(size(like.height)) || 0);
}
const plan = sceneReferences(scene, project.fixed_references);
const { overrides, unwired } = wireReferences(plan.ids, slots, source ? [source.slot.id] : [], plan.pins);
for (const [id, fields] of Object.entries(overrides)) raw[id] = { ...raw[id], ...fields };
+10
View File
@@ -82,3 +82,13 @@ test("the shot camera node is told which project this is, whatever roles the sav
const { inputs } = await buildInputs({ project: { id: "proj1", scenes: [], video: {} }, scene: { id: "s", text: "x", references: [] }, slots, expand: async ({ text }) => ({ text }) });
assert.equal(inputs.shot_camera.project, "proj1");
});
test("text-only: a slot that can stand in for the picture is told the size of the slot that shapes it, and a picked picture is sent as is", async () => {
const like = { slot: "t", width: "target_width", height: "target_height" };
const s = [{ id: "src", roles: [{ at: "assets.source_image", input: "media_id", blank_like: like }] },
{ id: "t", inputs: { target_width: 1344, target_height: 768 }, roles: [{ at: "project.video", input: "target_width" }] }];
const run = (project, scene) => buildInputs({ project, scene, slots: s, expand: async () => null });
assert.deepEqual((await run({ generation_mode: "t2v", video: { target_width: 640 } }, { source_image: "m1" })).inputs.src, { media_id: "", blank_width: 640, blank_height: 768 });
assert.deepEqual((await run({ generation_mode: "i2v" }, { source_image: "m1" })).inputs.src, { media_id: "m1" });
assert.deepEqual((await run({ generation_mode: "i2v" }, {})).inputs.src, { media_id: "", blank_width: 1344, blank_height: 768 });
});
+1 -1
View File
@@ -81,7 +81,7 @@ FROZEN = {
"model", "lora_name", "strength_model", "clip", "strength_clip",
],
"FunPackVAELoader": ["vae_name", "dtype"],
"FunPackLoadMedia": ["media_id"],
"FunPackLoadMedia": ["media_id", "blank_width", "blank_height"],
}
+12 -1
View File
@@ -10,6 +10,7 @@ a run queues, the same way a scene's prompt text lands on CLIPTextEncode's
`text`), not from someone typing a file name into this node by hand.
"""
import torch
from comfy_api.latest import InputImpl, io
from ..._core import log, media as media_mod
@@ -27,13 +28,23 @@ class FunPackLoadMedia(io.ComfyNode):
io.String.Input(
"media_id", default="",
tooltip="A media library id, picked in the Media bin."),
io.Int.Input(
"blank_width", default=0, min=0, max=16384, optional=True,
tooltip="With nothing picked, make a plain grey picture this wide instead of refusing. 0 = refuse."),
io.Int.Input(
"blank_height", default=0, min=0, max=16384, optional=True,
tooltip="With nothing picked, make a plain grey picture this tall instead of refusing. 0 = refuse."),
],
outputs=[io.Image.Output(display_name="image")],
)
@classmethod
def execute(cls, media_id: str) -> io.NodeOutput:
def execute(cls, media_id: str, blank_width: int = 0, blank_height: int = 0) -> io.NodeOutput:
mid = (media_id or "").strip()
if not mid and blank_width > 0 and blank_height > 0:
# A text-only start: the picture exists only to give the pipeline a SHAPE (see the H3 preset).
log.info("FunPack Load Media", f"nothing picked: a plain {blank_width}x{blank_height} picture stands in")
return io.NodeOutput(torch.full((1, blank_height, blank_width, 3), 0.5))
if not mid:
raise RuntimeError(
"FunPack Load Media: nothing is picked for this input. "
@@ -59,3 +59,14 @@ def test_deleted_media_is_refused_even_with_a_well_formed_id(store):
media.delete(entry["id"])
with pytest.raises(RuntimeError, match="not in the library"):
FunPackLoadMedia.execute(entry["id"]).result
def test_a_blank_size_stands_in_for_nothing_picked_and_a_picked_image_still_wins(store):
from modules.media.load.nodes import FunPackLoadMedia
images, = FunPackLoadMedia.execute("", blank_width=64, blank_height=32).result
assert tuple(images.shape) == (1, 32, 64, 3)
entry = media.save_upload("ref.png", _png_bytes())
images, = FunPackLoadMedia.execute(entry["id"], blank_width=64, blank_height=32).result
assert images.shape[1:3] == (6, 4)
with pytest.raises(RuntimeError, match="nothing is picked"):
FunPackLoadMedia.execute("", blank_width=64, blank_height=0).result
+5 -2
View File
@@ -61,8 +61,11 @@ def h3_reference_to_video():
# role's name and sets `media_id` from the scene's own picked image,
# the same way "generation.prompt" is found by name and set from the
# scene's own text -- neither is addressed by this preset's slot id.
"roles": [{"at": "assets.source_image", "input": "media_id"}],
"inputs": {"media_id": ""}},
# `blank_like`: with no picture (text-only), buildInputs gives this node the size of that
# slot's target_width/height, so a plain grey stand-in carries the project's own shape.
"roles": [{"at": "assets.source_image", "input": "media_id",
"blank_like": {"slot": "image_transform", "width": "target_width", "height": "target_height"}}],
"inputs": {"media_id": "", "blank_width": 0, "blank_height": 0}},
{"id": "image_transform", "group": "Reference media", "node": "ImageTransformKJ",
# target_width/height are the PIXEL BUDGET, set in Project settings
# like any other project resolution -- not the dropped picture's own