Files
vllm-omni/tests/e2e/features/comfyui/test_latent_mask_workflows.py
T
bad88bca25 [Frontend] MiniMax-H3: Add latent-mask editing to the ComfyUI extension (#7575)
Signed-off-by: chen hongwei <1792043268@qq.com>
Signed-off-by: Wang Zhipeng <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Co-authored-by: Wang Zhipeng <wangzhipeng628@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:32:40 +00:00

98 lines
4.0 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import json
from graphlib import TopologicalSorter
from pathlib import Path
import pytest
from comfyui_vllm_omni import nodes as omni_nodes
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
WORKFLOW = (
Path(__file__).resolve().parents[4]
/ "apps/ComfyUI-vLLM-Omni/example_workflows/vLLM-Omni MiniMax-H3 Latent Mask Editing.json"
)
@pytest.fixture
def workflow():
return json.loads(WORKFLOW.read_text())
def _source_of(workflow: dict, node: dict, input_name: str) -> dict:
nodes = {n["id"]: n for n in workflow["nodes"]}
links = {link[0]: link for link in workflow["links"]}
link_id = next(i["link"] for i in node["inputs"] if i["name"] == input_name)
assert link_id is not None, f"{node['type']}.{input_name} is not connected"
return nodes[links[link_id][1]]
def _cases(workflow: dict) -> dict[str, dict]:
return {
node["title"].removeprefix("Generate Video(").removesuffix(")"): node
for node in workflow["nodes"]
if node["type"] == "VLLMOmniGenerateVideo"
}
def test_latent_mask_workflow_connections(workflow):
nodes = {node["id"]: node for node in workflow["nodes"]}
links = {link[0]: link for link in workflow["links"]}
assert len(nodes) == len(workflow["nodes"])
assert len(links) == len(workflow["links"])
graph: dict[int, set[int]] = {node_id: set() for node_id in nodes}
for link_id, source, output_slot, target, input_slot, kind in links.values():
output = nodes[source]["outputs"][output_slot]
input_ = nodes[target]["inputs"][input_slot]
assert output["type"] == input_["type"] == kind
assert link_id in output["links"]
assert input_["link"] == link_id
graph[target].add(source)
assert len(tuple(TopologicalSorter(graph).static_order())) == len(nodes)
def test_latent_mask_workflow_matches_omni_node_interfaces(workflow):
for node in workflow["nodes"]:
if not node["type"].startswith("VLLMOmni"):
continue
cls = getattr(omni_nodes, node["type"])
schema = cls.INPUT_TYPES()
inputs = {**schema.get("required", {}), **schema.get("optional", {})}
for input_ in node["inputs"]:
kind = inputs[input_["name"]][0]
assert input_["type"] == ("COMBO" if isinstance(kind, list) else kind)
assert tuple(output["type"] for output in node["outputs"]) == cls.RETURN_TYPES
def test_latent_mask_workflow_defaults(workflow):
cases = _cases(workflow)
assert set(cases) == {"Object Removal", "Inpainting", "Continuation", "Extension"}
for generate in cases.values():
assert generate["widgets_values"][0] == "http://127.0.0.1:8000/v1"
assert generate["widgets_values"][4:6] == [1344, 768]
@pytest.mark.parametrize("case", ["Object Removal", "Inpainting"])
def test_spatial_cases_use_a_composited_mask(workflow, case):
edit = _source_of(workflow, _cases(workflow)[case], "latent_edit")
assert _source_of(workflow, edit, "source_video")["type"] == "LoadVideo"
assert _source_of(workflow, edit, "video_mask")["type"] == "MaskComposite"
@pytest.mark.parametrize("case", ["Continuation", "Extension"])
def test_temporal_cases_build_a_per_frame_mask(workflow, case):
# The server pads a short [T, H, W] mask with its last slice, so the mask must
# come from the Temporal Mask node (one slice per output frame), not a
# hand-batched stack of a few slices.
generate = _cases(workflow)[case]
edit = _source_of(workflow, generate, "latent_edit")
temporal = _source_of(workflow, edit, "video_mask")
assert temporal["type"] == "VLLMOmniMiniMaxH3TemporalMask"
assert _source_of(workflow, temporal, "images")["type"] == "GetVideoComponents"
assert _source_of(workflow, edit, "source_video")["type"] == "LoadVideo"
_, duration, mode, _ = temporal["widgets_values"]
assert mode == case.lower()
assert duration == generate["widgets_values"][7]