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:
co-authored by
Claude Sonnet 5.5
parent
fd3224d9a4
commit
1fc5d99dcc
@@ -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 };
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user