fix(HEL-623): split dual-prompt text encoders; positive wiring wins; detect duration via expression
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
063a3c7063
commit
0031a897d7
@@ -78,6 +78,7 @@ _SAMPLER_CLASSES = frozenset({"KSampler", "KSamplerAdvanced"})
|
||||
_NEGATIVE_WORDS = ("negative", "负面", "反向", "反面")
|
||||
_SYSTEM_WORDS = ("system", "系统")
|
||||
_PROMPT_WORDS = ("prompt", "positive", "提示", "正向")
|
||||
_NEGATIVE_TEXT_FIELDS = ("negative_prompt", "negative_text", "negative")
|
||||
|
||||
# Title keyword → (role, Chinese label) for untagged numeric primitives.
|
||||
_NUMBER_TITLE_ROLES: tuple[tuple[tuple[str, ...], str, str], ...] = (
|
||||
@@ -121,22 +122,66 @@ def _consumers(workflow: dict[str, Any]) -> dict[str, list[tuple[str, str]]]:
|
||||
def _feeds_negative(
|
||||
node_id: str, consumers: dict[str, list[tuple[str, str]]], depth: int = 4
|
||||
) -> bool:
|
||||
"""True when this node's output reaches a sampler/guider ``negative`` input."""
|
||||
"""True when this node's output reaches a sampler/guider ``negative`` input
|
||||
and never a ``positive`` one.
|
||||
|
||||
A text encoder wired into both (one conditioning reused for positive and
|
||||
negative) is treated as the positive prompt; only a negative-sounding
|
||||
title makes it negative (see ``_is_negative``).
|
||||
"""
|
||||
frontier = [node_id]
|
||||
seen = {node_id}
|
||||
negative = False
|
||||
for _ in range(depth):
|
||||
nxt: list[str] = []
|
||||
for nid in frontier:
|
||||
for cid, name in consumers.get(nid, []):
|
||||
if name == "negative":
|
||||
return True
|
||||
if name == "positive":
|
||||
return False
|
||||
if name == "negative":
|
||||
negative = True
|
||||
continue
|
||||
if cid not in seen:
|
||||
seen.add(cid)
|
||||
nxt.append(cid)
|
||||
frontier = nxt
|
||||
return False
|
||||
return negative
|
||||
|
||||
|
||||
_FRAME_INPUTS = frozenset({"length", "num_frames", "frames", "video_length", "frame_count"})
|
||||
_SECONDS_INPUTS = frozenset({"duration", "seconds", "duration_seconds"})
|
||||
_EXPRESSION_HINTS = ("expression", "math")
|
||||
|
||||
|
||||
def _duration_hint(
|
||||
node_id: str,
|
||||
workflow: dict[str, Any],
|
||||
consumers: dict[str, list[tuple[str, str]]],
|
||||
depth: int = 3,
|
||||
) -> str | None:
|
||||
"""How an untitled number feeds the video length downstream.
|
||||
|
||||
``"seconds"`` — it reaches a seconds input, or a frame-count input only
|
||||
after passing through a math/expression node (seconds × fps → frames);
|
||||
``"frames"`` — it is wired straight into a frame-count input.
|
||||
"""
|
||||
frontier = [(node_id, False)]
|
||||
seen = {node_id}
|
||||
for _ in range(depth):
|
||||
nxt: list[tuple[str, bool]] = []
|
||||
for nid, via_expr in frontier:
|
||||
for cid, name in consumers.get(nid, []):
|
||||
if name in _SECONDS_INPUTS:
|
||||
return "seconds"
|
||||
if name in _FRAME_INPUTS:
|
||||
return "seconds" if via_expr else "frames"
|
||||
if cid in seen:
|
||||
continue
|
||||
seen.add(cid)
|
||||
ct = str((workflow.get(cid) or {}).get("class_type") or "").lower()
|
||||
nxt.append((cid, via_expr or any(h in ct for h in _EXPRESSION_HINTS)))
|
||||
frontier = nxt
|
||||
return None
|
||||
|
||||
|
||||
def _entry(
|
||||
@@ -224,7 +269,15 @@ def detect_untagged_inputs(workflow: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
continue
|
||||
if ct == "CLIPTextEncode" or "TextEncode" in ct:
|
||||
field = next((f for f in ("text", "prompt") if isinstance(inputs.get(f), str)), None)
|
||||
if field:
|
||||
neg_field = next(
|
||||
(f for f in _NEGATIVE_TEXT_FIELDS if isinstance(inputs.get(f), str)), None
|
||||
)
|
||||
if field and neg_field:
|
||||
# One encoder carrying both prompts (TextEncodeQwenImage21…):
|
||||
# its outputs feed positive *and* negative, so wiring says nothing.
|
||||
texts.append((nid, node, field, False))
|
||||
texts.append((nid, node, neg_field, True))
|
||||
elif field:
|
||||
texts.append((nid, node, field, _is_negative(nid, lowered, consumers)))
|
||||
continue
|
||||
|
||||
@@ -253,6 +306,12 @@ def detect_untagged_inputs(workflow: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if any(w in lowered for w in words):
|
||||
role, label = r, (title if title and title != ct else zh)
|
||||
break
|
||||
if role is None and (not title or title == ct):
|
||||
hint = _duration_hint(nid, workflow, consumers)
|
||||
if hint == "seconds":
|
||||
role, label = "duration", "时长(秒)"
|
||||
elif hint == "frames":
|
||||
label = "视频帧数"
|
||||
if role and role in taken_roles:
|
||||
role = None
|
||||
if role:
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
{
|
||||
"1": {
|
||||
"inputs": {
|
||||
"value": "PASTE YOUR PROMPT HERE. Use the MiniMax-H3 prompt contract (integrated_multimodal_description / overall_soundscape / non_diegetic_music). The previews in this repo were made with a ten-shot subway prompt posted by a community member; it is not redistributed here."
|
||||
},
|
||||
"class_type": "PrimitiveStringMultiline",
|
||||
"_meta": {
|
||||
"title": "PROMPT (feeds every stage)"
|
||||
}
|
||||
},
|
||||
"92": {
|
||||
"inputs": {
|
||||
"filename_prefix": "video/01_reference_4step_sla",
|
||||
"format": "auto",
|
||||
"format.codec": "auto",
|
||||
"codec": "auto",
|
||||
"video": [
|
||||
"130",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveVideo",
|
||||
"_meta": {
|
||||
"title": "REFERENCE 2026-08-29: fused ref-delta bake + SLA 0.9/64, res_multistep/simple/4, shift 12/3, 1152x640, 243 f, seed 42 -> subway_bake_sla_00001_.mp4"
|
||||
}
|
||||
},
|
||||
"115": {
|
||||
"inputs": {
|
||||
"aspect_ratio": "16:9 (Widescreen)",
|
||||
"megapixels": 0.7,
|
||||
"multiple": 32
|
||||
},
|
||||
"class_type": "ResolutionSelector",
|
||||
"_meta": {
|
||||
"title": "ResolutionSelector"
|
||||
}
|
||||
},
|
||||
"116": {
|
||||
"inputs": {
|
||||
"expression": "a"
|
||||
},
|
||||
"class_type": "ComfyMathExpression",
|
||||
"_meta": {
|
||||
"title": "ComfyMathExpression"
|
||||
}
|
||||
},
|
||||
"119": {
|
||||
"inputs": {
|
||||
"vae_name": "minimax_h3_video_vae_int8_convrot.safetensors"
|
||||
},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": {
|
||||
"title": "VAELoader"
|
||||
}
|
||||
},
|
||||
"120": {
|
||||
"inputs": {
|
||||
"vae_name": "minimax_h3_audio_vae_fp32.safetensors"
|
||||
},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": {
|
||||
"title": "VAELoader"
|
||||
}
|
||||
},
|
||||
"121": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"125",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"120",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecodeAudio",
|
||||
"_meta": {
|
||||
"title": "VAEDecodeAudio"
|
||||
}
|
||||
},
|
||||
"122": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"125",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"119",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAEDecode"
|
||||
}
|
||||
},
|
||||
"124": {
|
||||
"inputs": {
|
||||
"scheduler": "simple",
|
||||
"steps": 4,
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"142",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicScheduler",
|
||||
"_meta": {
|
||||
"title": "BasicScheduler"
|
||||
}
|
||||
},
|
||||
"125": {
|
||||
"inputs": {
|
||||
"noise": [
|
||||
"129",
|
||||
0
|
||||
],
|
||||
"guider": [
|
||||
"126",
|
||||
0
|
||||
],
|
||||
"sampler": [
|
||||
"135",
|
||||
0
|
||||
],
|
||||
"sigmas": [
|
||||
"124",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"131",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "SamplerCustomAdvanced",
|
||||
"_meta": {
|
||||
"title": "SamplerCustomAdvanced"
|
||||
}
|
||||
},
|
||||
"126": {
|
||||
"inputs": {
|
||||
"model": [
|
||||
"142",
|
||||
0
|
||||
],
|
||||
"conditioning": [
|
||||
"131",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicGuider",
|
||||
"_meta": {
|
||||
"title": "BasicGuider"
|
||||
}
|
||||
},
|
||||
"127": {
|
||||
"inputs": {
|
||||
"unet_name": "minimax_h3_fused_refdelta_r1024_turbo8_mystic07_int8_convrot.safetensors",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"_meta": {
|
||||
"title": "UNETLoader"
|
||||
}
|
||||
},
|
||||
"128": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen3vl_32b_heretic_minimax_h3_nvfp4.safetensors",
|
||||
"type": "minimax",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": {
|
||||
"title": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
"129": {
|
||||
"inputs": {
|
||||
"noise_seed": 42
|
||||
},
|
||||
"class_type": "RandomNoise",
|
||||
"_meta": {
|
||||
"title": "RandomNoise"
|
||||
}
|
||||
},
|
||||
"130": {
|
||||
"inputs": {
|
||||
"fps": 24,
|
||||
"bit_depth": 8,
|
||||
"color_space": "sRGB",
|
||||
"codec": "none",
|
||||
"images": [
|
||||
"122",
|
||||
0
|
||||
],
|
||||
"audio": [
|
||||
"121",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "CreateVideo",
|
||||
"_meta": {
|
||||
"title": "CreateVideo"
|
||||
}
|
||||
},
|
||||
"131": {
|
||||
"inputs": {
|
||||
"prompt": [
|
||||
"1",
|
||||
0
|
||||
],
|
||||
"width": [
|
||||
"115",
|
||||
0
|
||||
],
|
||||
"height": [
|
||||
"115",
|
||||
1
|
||||
],
|
||||
"length": [
|
||||
"132",
|
||||
1
|
||||
],
|
||||
"clip": [
|
||||
"128",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"119",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "MiniMaxH3ImageToVideo",
|
||||
"_meta": {
|
||||
"title": "MiniMaxH3ImageToVideo"
|
||||
}
|
||||
},
|
||||
"132": {
|
||||
"inputs": {
|
||||
"expression": "max(5, round(a * 24)) + (5 - (max(5, round(a * 24)) % 17)) % 17",
|
||||
"values.a": [
|
||||
"133",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "ComfyMathExpression",
|
||||
"_meta": {
|
||||
"title": "ComfyMathExpression"
|
||||
}
|
||||
},
|
||||
"133": {
|
||||
"inputs": {
|
||||
"value": 10
|
||||
},
|
||||
"class_type": "PrimitiveFloat",
|
||||
"_meta": {
|
||||
"title": "PrimitiveFloat"
|
||||
}
|
||||
},
|
||||
"135": {
|
||||
"inputs": {
|
||||
"sampler_name": "res_multistep"
|
||||
},
|
||||
"class_type": "KSamplerSelect",
|
||||
"_meta": {
|
||||
"title": "KSamplerSelect"
|
||||
}
|
||||
},
|
||||
"142": {
|
||||
"inputs": {
|
||||
"shift_video": 12,
|
||||
"shift_audio": 3,
|
||||
"model": [
|
||||
"900",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "MiniMaxH3SigmaShift",
|
||||
"_meta": {
|
||||
"title": "MiniMaxH3SigmaShift"
|
||||
}
|
||||
},
|
||||
"900": {
|
||||
"inputs": {
|
||||
"sparsity_ratio": 0.9,
|
||||
"block_size": "64",
|
||||
"min_seq_len": 8192,
|
||||
"dense_last_steps": 0,
|
||||
"protect_audio": true,
|
||||
"enabled": true,
|
||||
"model": [
|
||||
"127",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "H3SLAAttention",
|
||||
"_meta": {
|
||||
"title": "H3 SLA Attention 0.9/64 (their graph)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"13": {
|
||||
"inputs": {
|
||||
"aspect_ratio": "1:1 (Square)",
|
||||
"megapixels": 1,
|
||||
"multiple": 8
|
||||
},
|
||||
"class_type": "ResolutionSelector",
|
||||
"_meta": {
|
||||
"title": "分辨率选择器"
|
||||
}
|
||||
},
|
||||
"461": {
|
||||
"inputs": {
|
||||
"filename_prefix": "Qwen_image_2.1",
|
||||
"format": "png",
|
||||
"format.bit_depth": "8-bit",
|
||||
"format.input_color_space": "sRGB",
|
||||
"images": [
|
||||
"459:457",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImageAdvanced",
|
||||
"_meta": {
|
||||
"title": "保存图像(高级)"
|
||||
}
|
||||
},
|
||||
"459:451": {
|
||||
"inputs": {
|
||||
"unet_name": "qwen_image_2.1_int8_convrot.safetensors",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader",
|
||||
"_meta": {
|
||||
"title": "UNet加载器"
|
||||
}
|
||||
},
|
||||
"459:452": {
|
||||
"inputs": {
|
||||
"prompt": "Greyscale fashion editorial portrait of an avant-garde woman tilted upwards in profile, captured with striking high-contrast chiaroscuro lighting. She wears oversized, thick-rimmed circular black sunglasses with matte dark lenses, concealing her upward gaze. Her attire merges high couture with tactical abstraction: a high-necked structural dress tailored from heavyweight fabric featuring a disrupted optical camouflage pattern—interlocking oversized polka dots, pixelated stippling, and organic contour blotches in stark monochrome that mimic military disruptive patterns in a refined runway style. Around her shoulders, asymmetric pleated webbing and tailored cargo straps are integrated seamlessly into the garment’s architecture. In her gloved hand, she clutches a structured matte-leather geometric handbag accented with subtle tactical buckles, utility stitching, and dark brushed-metal hardware. Her skin texture is rendered with hyper-detailed clarity, sculpted by intense, razor-sharp studio key light and deep graphite shadows. The striking background is a vivid mixed-media graphic collage, composed of flat vibrant lime green planes, stark black and white overlapping circles, segmented semi-circles with bold zebra striping, dense micro-halftone dot matrices, and vector topographic contour lines. The clash between monochromatic disruptive camo elements on her clothing and the saturated, retro-futuristic pop-art backdrop creates an intense visual dissonance, emphasizing sharp silhouettes, paper cut-out edges, dynamic compositional balance, and clean graphic novel textures. Absolutely no text, no typography, no letters, no words, no numbers, no logos, no watermark, no captions, pure imagery only.",
|
||||
"negative_prompt": "",
|
||||
"resolution": 1024,
|
||||
"clip": [
|
||||
"459:453",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "TextEncodeQwenImage21",
|
||||
"_meta": {
|
||||
"title": "Text Encode Qwen Image 2.1"
|
||||
}
|
||||
},
|
||||
"459:453": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen3vl_8b_int8_convrot.safetensors",
|
||||
"type": "qwen_image",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": {
|
||||
"title": "加载CLIP"
|
||||
}
|
||||
},
|
||||
"459:454": {
|
||||
"inputs": {
|
||||
"vae_name": "qwen_image_2.1_vae_bf16.safetensors"
|
||||
},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": {
|
||||
"title": "加载VAE"
|
||||
}
|
||||
},
|
||||
"459:456": {
|
||||
"inputs": {
|
||||
"width": [
|
||||
"13",
|
||||
0
|
||||
],
|
||||
"height": [
|
||||
"13",
|
||||
1
|
||||
],
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage",
|
||||
"_meta": {
|
||||
"title": "空Latent图像"
|
||||
}
|
||||
},
|
||||
"459:457": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"459:458",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"459:454",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAE解码"
|
||||
}
|
||||
},
|
||||
"459:458": {
|
||||
"inputs": {
|
||||
"seed": 593103825222985,
|
||||
"steps": 25,
|
||||
"cfg": 1,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"459:451",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"459:452",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"459:452",
|
||||
1
|
||||
],
|
||||
"latent_image": [
|
||||
"459:456",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "KSampler",
|
||||
"_meta": {
|
||||
"title": "K采样器"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,85 @@ def test_untagged_h3_video_detects_prompt_duration_seed():
|
||||
assert roles["seed"]["nodeId"] == "217@noise_seed"
|
||||
|
||||
|
||||
def test_real_qwen21_t2i_splits_single_encoder_into_both_prompts():
|
||||
"""白栖知的 wen-sheng-tu.json:一个 TextEncodeQwenImage21 同时喂 positive/negative."""
|
||||
wf = _load("wen_sheng_tu_API.json")
|
||||
inputs = parse_dynamic_inputs(wf)
|
||||
roles = _by_role(inputs)
|
||||
assert roles["prompt"]["nodeId"] == "459:452@prompt"
|
||||
assert roles["prompt"]["label"] == "正向提示词"
|
||||
assert roles["prompt"]["required"] is True
|
||||
assert roles["prompt"]["defaultValue"].startswith("Greyscale fashion editorial")
|
||||
assert roles["negative"]["nodeId"] == "459:452@negative_prompt"
|
||||
assert roles["negative"]["label"] == "反向提示词"
|
||||
assert roles["negative"]["required"] is False
|
||||
assert roles["seed"]["nodeId"] == "459:458@seed"
|
||||
assert roles["steps"]["nodeId"] == "459:458@steps"
|
||||
assert roles["cfg"]["nodeId"] == "459:458@cfg"
|
||||
assert roles["batch"]["nodeId"] == "459:456@batch_size"
|
||||
# width/height are wired from the resolution selector — not exposed
|
||||
assert "width" not in roles and "height" not in roles
|
||||
assert [i for i in inputs if i["required"]] == [roles["prompt"]]
|
||||
|
||||
patched = patch_workflow(
|
||||
wf, {roles["prompt"]["nodeId"]: "新正向", roles["negative"]["nodeId"]: "模糊"}
|
||||
)
|
||||
enc = patched["459:452"]["inputs"]
|
||||
assert enc["prompt"] == "新正向"
|
||||
assert enc["negative_prompt"] == "模糊"
|
||||
assert enc["clip"] == ["459:453", 0]
|
||||
|
||||
|
||||
def test_real_h3_t2v_duration_from_expression_primitive():
|
||||
"""wen-sheng-shipin.json:无标题 PrimitiveFloat 经表达式换算帧数 → 时长(秒)."""
|
||||
inputs = parse_dynamic_inputs(_load("wen_sheng_shipin_API.json"))
|
||||
roles = _by_role(inputs)
|
||||
assert roles["prompt"]["nodeId"] == "1"
|
||||
assert roles["prompt"]["required"] is True
|
||||
assert roles["duration"]["nodeId"] == "133"
|
||||
assert roles["duration"]["label"] == "时长(秒)"
|
||||
assert roles["duration"]["defaultValue"] == 10
|
||||
assert roles["seed"]["nodeId"] == "129@noise_seed"
|
||||
assert roles["steps"]["nodeId"] == "124@steps"
|
||||
assert "negative" not in roles
|
||||
|
||||
|
||||
def _encoder_graph(encoder: dict, positive_slot: list, negative_slot: list) -> dict:
|
||||
return {
|
||||
"1": encoder,
|
||||
"2": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {"seed": 1, "steps": 20, "cfg": 4, "positive": positive_slot,
|
||||
"negative": negative_slot},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_custom_encoder_with_text_and_negative_prompt_is_split():
|
||||
wf = _encoder_graph(
|
||||
{"class_type": "MyTextEncodeDual", "inputs": {"text": "猫", "negative_prompt": "糊"},
|
||||
"_meta": {"title": "MyTextEncodeDual"}},
|
||||
["1", 0],
|
||||
["1", 1],
|
||||
)
|
||||
roles = _by_role(parse_dynamic_inputs(wf))
|
||||
assert roles["prompt"]["nodeId"] == "1"
|
||||
assert roles["prompt"]["defaultValue"] == "猫"
|
||||
assert roles["negative"]["nodeId"] == "1@negative_prompt"
|
||||
|
||||
|
||||
def test_clip_encoder_wired_to_both_positive_and_negative_stays_positive():
|
||||
wf = _encoder_graph(
|
||||
{"class_type": "CLIPTextEncode", "inputs": {"text": "猫"},
|
||||
"_meta": {"title": "CLIP Text Encode"}},
|
||||
["1", 0],
|
||||
["1", 0],
|
||||
)
|
||||
roles = _by_role(parse_dynamic_inputs(wf))
|
||||
assert roles["prompt"]["nodeId"] == "1"
|
||||
assert "negative" not in roles
|
||||
|
||||
|
||||
def test_tagged_workflows_are_unchanged():
|
||||
for path in sorted(EXAMPLES.glob("*.json")):
|
||||
inputs = parse_dynamic_inputs(json.loads(path.read_text(encoding="utf-8")))
|
||||
@@ -158,6 +237,35 @@ def test_native_workflow_full_chain(client, fake_comfy): # noqa: F811
|
||||
assert any(j["id"] == job["id"] and j["status"] == "done" for j in history)
|
||||
|
||||
|
||||
def test_real_qwen21_t2i_full_chain(client, fake_comfy): # noqa: F811
|
||||
with _real_run(client, fake_comfy):
|
||||
wf_id = _import_workflow(
|
||||
client, "真实·Qwen2.1 文生图", "image", _load("wen_sheng_tu_API.json")
|
||||
)
|
||||
roles = _by_role(client.get(f"/api/workflows/{wf_id}").json()["input_schema"])
|
||||
r = client.post(
|
||||
"/api/playground/generate",
|
||||
json={
|
||||
"workflow_id": wf_id,
|
||||
"input_values": {
|
||||
roles["prompt"]["nodeId"]: "雨夜霓虹街道",
|
||||
roles["negative"]["nodeId"]: "低清晰度",
|
||||
roles["seed"]["nodeId"]: 7,
|
||||
},
|
||||
"source": "studio-image",
|
||||
"endpoint_id": settings.comfyui_active_id,
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
job = _wait_done(client, r.json()["job"]["id"])
|
||||
assert job["status"] == "done", job.get("error")
|
||||
queued = fake_comfy.prompts[-1]["prompt"]
|
||||
assert queued["459:452"]["inputs"]["prompt"] == "雨夜霓虹街道"
|
||||
assert queued["459:452"]["inputs"]["negative_prompt"] == "低清晰度"
|
||||
assert queued["459:458"]["inputs"]["seed"] == 7
|
||||
assert queued["459:458"]["inputs"]["negative"] == ["459:452", 1]
|
||||
|
||||
|
||||
def test_comfy_rejection_is_reported_in_chinese(client, fake_comfy): # noqa: F811
|
||||
fake_comfy.fail_prompt = True
|
||||
with _real_run(client, fake_comfy):
|
||||
|
||||
Reference in New Issue
Block a user