Files
ComfyAssets-ComfyUI_PromptM…/tests/generation_params_cases.py
T
Vito a44cd27c2e fix(parse): build array String() without recursion (#255)
utils.generation_params converted nested arrays to strings recursively,
so a crafted file with a value nested about 900 arrays deep raised
RecursionError, where the browser parser returns a result. The join now
runs on an explicit stack. A JS-parity case with 900-deep values pins it.
2026-10-19 22:04:32 -07:00

462 lines
13 KiB
Python

"""Named workflow cases shared by the generation-parameter parser tests.
Each case is the raw metadata records a file would carry, as text, so the cases
exercise ``parse_js_json`` as well as the tracing. The JS parity test runs every
case through the legacy browser parser too. Prompt texts and seeds are invented.
"""
import json
def _text(value):
return None if value is None else json.dumps(value)
def case(prompt=None, workflow=None):
return {"prompt": _text(prompt), "workflow": _text(workflow)}
def encoder(text, clip=("4", 1)):
return {
"class_type": "CLIPTextEncode",
"inputs": {"text": text, "clip": list(clip)},
}
def ksampler(positive, negative, model=("4", 0), **settings):
inputs = {
"seed": 5,
"steps": 20,
"cfg": 7,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1.0,
"model": list(model),
"positive": list(positive),
"negative": list(negative),
"latent_image": ["2", 0],
}
inputs.update(settings)
return {"class_type": "KSampler", "inputs": inputs}
CHECKPOINT = {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "example.safetensors"},
}
LATENT = {
"class_type": "EmptyLatentImage",
"inputs": {"width": 512, "height": 512, "batch_size": 1},
}
def _basic():
return {
"4": CHECKPOINT,
"2": LATENT,
"6": encoder("a cat on a windowsill"),
"7": encoder("blurry, watermark"),
"3": ksampler(("6", 0), ("7", 0)),
"9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0]}},
}
def _prompt_manager(text, prepend="", append=""):
return {
"class_type": "PromptManager",
"inputs": {
"text": text,
"category": "",
"tags": "",
"search_text": "",
"prepend_text": prepend,
"append_text": append,
"clip": ["4", 1],
},
}
def _shared_prompt_pipeline():
"""One PromptManager pair feeds a base pass, an upscale pass and a detailer."""
return {
"1": CHECKPOINT,
"5": ksampler(
("25", 0),
("26", 0),
("1", 0),
seed=111,
steps=4,
cfg=1.0,
sampler_name="lcm",
latent_image=["20", 0],
),
"6": {
"class_type": "VAEDecode",
"inputs": {"samples": ["5", 0], "vae": ["1", 2]},
},
"11": {
"class_type": "VAEEncode",
"inputs": {"pixels": ["6", 0], "vae": ["1", 2]},
},
"12": ksampler(
("25", 0),
("26", 0),
("1", 0),
seed=222,
steps=6,
cfg=1.0,
sampler_name="lcm",
denoise=0.45,
latent_image=["11", 0],
),
"13": {
"class_type": "VAEDecode",
"inputs": {"samples": ["12", 0], "vae": ["1", 2]},
},
"17": {
"class_type": "FaceDetailer",
"inputs": {
"seed": 333,
"steps": 4,
"cfg": 1.0,
"sampler_name": "lcm",
"image": ["13", 0],
"model": ["1", 0],
"clip": ["1", 1],
"vae": ["1", 2],
"positive": ["25", 0],
"negative": ["26", 0],
},
},
"20": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 640, "height": 1536, "batch_size": 1},
},
"25": _prompt_manager("a lighthouse at dusk, oil painting"),
"26": _prompt_manager("blurry, watermark"),
"48": {
"class_type": "KikoSaveImage",
"inputs": {"filename_prefix": ["49", 0], "images": ["17", 0]},
},
"49": {
"class_type": "KikoProjectFilePath",
"inputs": {"project_name": "example"},
},
}
def _custom_sampler(guider):
return {
"13": CHECKPOINT,
"20": encoder("a red fox in snow", clip=("13", 1)),
"21": encoder("lowres", clip=("13", 1)),
"22": guider,
"25": {"class_type": "RandomNoise", "inputs": {"noise_seed": 42}},
"16": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "dpmpp_2m"}},
"17": {
"class_type": "BasicScheduler",
"inputs": {
"scheduler": "karras",
"steps": 8,
"denoise": 1.0,
"model": ["13", 0],
},
},
"1300:8": {
"class_type": "SamplerCustomAdvanced",
"inputs": {
"noise": ["25", 0],
"guider": ["22", 0],
"sampler": ["16", 0],
"sigmas": ["17", 0],
"latent_image": ["5", 0],
},
},
}
def _workflow_only():
return {
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"mode": 0,
"inputs": [],
"widgets_values": ["example.safetensors"],
},
{
"id": 6,
"type": "CLIPTextEncode",
"mode": 0,
"inputs": [{"name": "clip", "link": 3}],
"widgets_values": ["a tidepool at noon"],
},
{
"id": 7,
"type": "CLIPTextEncode",
"mode": 0,
"inputs": [{"name": "clip", "link": 5}],
"widgets_values": ["noisy"],
},
{
"id": 10,
"type": "Reroute",
"mode": 0,
"inputs": [{"name": "", "link": 7}],
},
{
"id": 3,
"type": "KSampler",
"mode": 0,
"inputs": [
{"name": "model", "link": 1},
{"name": "positive", "link": 8},
{"name": "negative", "link": 4},
{"name": "latent_image", "link": 2},
],
"widgets_values": [314, "randomize", 25, 6.5, "dpmpp_2m", "karras", 1],
},
{
"id": 11,
"type": "CLIPTextEncode",
"mode": 2,
"inputs": [],
"widgets_values": ["muted node text"],
},
],
"links": [
[1, 4, 0, 3, 0, "MODEL"],
[3, 4, 1, 6, 0, "CLIP"],
[5, 4, 1, 7, 0, "CLIP"],
[7, 6, 0, 10, 0, "CONDITIONING"],
[8, 10, 0, 3, 1, "CONDITIONING"],
[4, 7, 0, 3, 2, "CONDITIONING"],
{
"id": 2,
"origin_id": 99,
"origin_slot": 0,
"target_id": 3,
"target_slot": 3,
"type": "LATENT",
},
],
}
def _nested(depth):
"""A value nested ``depth`` arrays deep, as a crafted file can hold."""
value = ["leaf", 7]
for _ in range(depth):
value = [value, None]
return value
def _chain(length):
graph = {"4": CHECKPOINT, "1": encoder("deep chain text")}
previous = "1"
for index in range(length):
node_id = str(100 + index)
graph[node_id] = {
"class_type": "ConditioningPass",
"inputs": {"conditioning": [previous, 0]},
}
previous = node_id
graph["3"] = ksampler((previous, 0), ("1", 0))
return graph
CASES = {
"basic": case(_basic()),
"prompt-manager-prepend-append": case(
{
"4": CHECKPOINT,
"6": _prompt_manager("a cat", prepend="masterpiece", append="4k"),
"7": _prompt_manager(
"masterpiece, a dog", prepend="masterpiece", append="4k"
),
"3": ksampler(("6", 0), ("7", 0)),
}
),
"linked-text-concat": case(
{
"4": CHECKPOINT,
"10": {"class_type": "PrimitiveString", "inputs": {"value": "first part"}},
"11": {
"class_type": "StringConcatenate",
"inputs": {
"string_a": ["10", 0],
"string_b": "second part",
"delimiter": ", ",
"seed": 3,
"string_c": " ",
},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": ["11", 0], "clip": ["4", 1]},
},
"7": encoder(""),
"3": ksampler(("6", 0), ("7", 0)),
}
),
"prompt-manager-pair": case(
{
"4": CHECKPOINT,
"8": {
"class_type": "PromptManagerPair",
"inputs": {
"text": "pair positive",
"negative_text": "pair negative",
"prepend_text": "",
"append_text": "",
"clip": ["4", 1],
},
},
"3": ksampler(("8", 0), ("8", 1)),
}
),
"conditioning-passthrough": case(
{
"4": CHECKPOINT,
"6": encoder("through a controlnet"),
"7": encoder("negative through it"),
"12": {
"class_type": "ControlNetApplyAdvanced",
"inputs": {
"positive": ["6", 0],
"negative": ["7", 0],
"control_net": ["13", 0],
},
},
"3": ksampler(("12", 0), ("12", 1)),
}
),
"first-sampler-in-js-key-order": case(
{
"4": CHECKPOINT,
"6": encoder("from sampler twelve"),
"7": encoder("from sampler five"),
"x": ksampler(("6", 0), ("6", 0)),
"12": ksampler(("6", 0), ("6", 0), seed=12),
"5": ksampler(("7", 0), ("7", 0), seed=55),
}
),
"custom-sampler-cfg-guider": case(
_custom_sampler(
{
"class_type": "CFGGuider",
"inputs": {
"model": ["13", 0],
"positive": ["20", 0],
"negative": ["21", 0],
"cfg": 3.5,
},
}
)
),
"custom-sampler-basic-guider": case(
_custom_sampler(
{
"class_type": "BasicGuider",
"inputs": {"model": ["13", 0], "conditioning": ["20", 0]},
}
)
),
"no-sampler-loose-settings": case(
{
"4": CHECKPOINT,
"3": {
"class_type": "MyKSamplerVariant",
"inputs": {
"noise_seed": 9,
"steps": 12,
"cfg": 2,
"sampler_name": "ddim",
},
},
}
),
"primitive-seed": case(
{
"4": CHECKPOINT,
"6": encoder("seeded"),
"30": {
"class_type": "Seed (rgthree)",
"inputs": {"other": "x", "seed": 77},
},
"3": ksampler(("6", 0), ("6", 0), seed=["30", 0]),
}
),
"workflow-only": case(workflow=_workflow_only()),
"overwritten-prompt-manager-text": case(
{
"4": CHECKPOINT,
"25": _prompt_manager("same saved text"),
"26": _prompt_manager("same saved text"),
"3": ksampler(("25", 0), ("26", 0)),
},
{
"nodes": [
{
"id": 25,
"type": "PromptManager",
"widgets_values": ["the real positive"],
},
{
"id": 26,
"type": "PromptManager",
"widgets_values": ["the real negative"],
},
],
"links": [],
},
),
"conditioning-cycle": case(
{
"4": CHECKPOINT,
"20": {
"class_type": "ConditioningCombine",
"inputs": {"conditioning_1": ["21", 0]},
},
"21": {
"class_type": "ConditioningCombine",
"inputs": {"conditioning_1": ["20", 0]},
},
"3": ksampler(("20", 0), ("21", 0)),
}
),
"chain-within-visit-limit": case(_chain(450)),
"chain-beyond-visit-limit": case(_chain(600)),
"chain-at-last-visit": case(_chain(499)),
"chain-one-past-last-visit": case(_chain(500)),
"blank-text-and-zero-seed": case(
{
"4": CHECKPOINT,
"6": encoder(" "),
"7": encoder("kept negative"),
"3": ksampler(("6", 0), ("7", 0), seed=0),
}
),
"shared-prompt-pipeline": case(_shared_prompt_pipeline()),
"null-workflow-node": case(_basic(), {"nodes": [None], "links": []}),
"unparsable-records": {"prompt": '{"3": NaN}', "workflow": "not json"},
"seed-beyond-double-precision": case(
{
"4": CHECKPOINT,
"6": encoder("x"),
"3": ksampler(("6", 0), ("6", 0), cfg=1.0, seed=4974052037430000001),
}
),
"deeply-nested-values": case(
{
"4": CHECKPOINT,
"6": encoder("nested neighbours"),
"3": ksampler(("6", 0), ("6", 0)),
"8": {"class_type": _nested(900), "inputs": {"text": "x"}},
},
{"nodes": [{"id": _nested(900), "type": _nested(900)}], "links": []},
),
"nothing": case(),
}