Files
Artificial-Sweetener-Simple…/tests/test_sdxl_visual_workflow.py
T

203 lines
7.1 KiB
Python

# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
# Copyright (C) 2026 Artificial Sweetener and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Verify one-case-at-a-time U11 managed SDXL workflow graphs."""
from __future__ import annotations
from pathlib import Path
from sdxl_visual_test_inventory import visual_inventory
from tools.comfy_api import JsonObject
from tools.sdxl_attention_coupling_integration.visual_cases import visual_cases
from tools.sdxl_attention_coupling_integration.visual_workflow import (
build_sdxl_visual_workflow,
)
def test_baseline_builds_only_one_native_full_sampler(tmp_path: Path) -> None:
"""Keep one source trajectory and four authored G/L encodes."""
case = next(
item
for item in visual_cases(visual_inventory(tmp_path))
if item.case_id == "baseline"
)
built = build_sdxl_visual_workflow(
run_id="run",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=case,
)
classes = _class_counts(built.prompt)
assert [output.artifact_id for output in built.outputs] == ["baseline--full"]
assert classes["SimpleSyrup.KSamplerAttentionCoupling"] == 1
assert "SimpleSyrup.KSamplerAttentionCouplingTiled" not in classes
assert "SimpleSyrup.KSamplerAttentionCouplingContextual" not in classes
assert classes["CLIPTextEncodeSDXL"] == 4
assert "CLIPTextEncode" not in classes
assert "CreateHookLora" not in classes
assert "LoraLoader" not in classes
sampler = next(
node["inputs"]
for node in built.prompt.values()
if node["class_type"] == "SimpleSyrup.KSamplerAttentionCoupling"
)
assert isinstance(sampler, dict)
assert sampler["regional_prompt_weight"] == 0.4
assert sampler["region_mask_feather"] == 0
def test_visual_workflow_applies_one_explicit_seed_to_every_sampler(
tmp_path: Path,
) -> None:
"""Keep every mode and durable workflow identity on one requested seed."""
case = next(
item
for item in visual_cases(visual_inventory(tmp_path))
if item.case_id == "spatial-mode-global-style-regional-character"
)
built = build_sdxl_visual_workflow(
run_id="seed-control",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=case,
seed=7_429_113_058,
)
assert built.seed == 7_429_113_058
assert _sampler_seeds(built.prompt) == {7_429_113_058}
def test_full_regional_control_changes_only_sampler_prompt_weight(
tmp_path: Path,
) -> None:
"""Emit the case-authored full strength through the public sampler input."""
cases = visual_cases(visual_inventory(tmp_path))
by_id = {case.case_id: case for case in cases}
control = build_sdxl_visual_workflow(
run_id="run-control",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=by_id["global-style-control"],
)
candidate = build_sdxl_visual_workflow(
run_id="run-candidate",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=by_id["global-style-full-regional-control"],
)
control_sampler = _sampler_inputs(control.prompt)
candidate_sampler = _sampler_inputs(candidate.prompt)
assert control_sampler["regional_prompt_weight"] == 0.4
assert candidate_sampler["regional_prompt_weight"] == 1.0
differing_inputs = {
name
for name in control_sampler
if control_sampler[name] != candidate_sampler[name]
}
assert differing_inputs == {"regional_prompt_weight"}
def test_global_style_character_pair_emits_full_regional_prompt_weight(
tmp_path: Path,
) -> None:
"""Keep the prompt control and regional-character candidate at full strength."""
by_id = {case.case_id: case for case in visual_cases(visual_inventory(tmp_path))}
for case_id in (
"global-style-right-character-prompt-control",
"global-style-regional-character",
):
built = build_sdxl_visual_workflow(
run_id=f"run-{case_id}",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=by_id[case_id],
)
assert _sampler_inputs(built.prompt)["regional_prompt_weight"] == 1.0
def test_selected_case_builds_full_tiled_and_contextual_from_one_source(
tmp_path: Path,
) -> None:
"""Refine only the accepted global-style plus regional-character source."""
case = next(
item
for item in visual_cases(visual_inventory(tmp_path))
if item.case_id == "spatial-mode-global-style-regional-character"
)
built = build_sdxl_visual_workflow(
run_id="run",
checkpoint_name=r"owned\checkpoint.safetensors",
mask_names=("left.png", "right.png"),
case=case,
)
classes = _class_counts(built.prompt)
assert [output.artifact_id for output in built.outputs] == [
"spatial-mode-global-style-regional-character--full",
"spatial-mode-global-style-regional-character--tiled-1.5x",
"spatial-mode-global-style-regional-character--contextual-1.5x",
]
assert classes["SimpleSyrup.KSamplerAttentionCoupling"] == 1
assert classes["SimpleSyrup.KSamplerAttentionCouplingTiled"] == 1
assert classes["SimpleSyrup.KSamplerAttentionCouplingContextual"] == 1
assert classes["ImageScale"] == 1
assert classes["VAEEncode"] == 1
assert classes["VAEDecode"] == 3
assert classes["LoraLoader"] == 1
assert classes["CreateHookLora"] == 1
def _class_counts(prompt: dict[str, JsonObject]) -> dict[str, int]:
"""Count exact API node classes for concise graph assertions."""
counts: dict[str, int] = {}
for node in prompt.values():
class_type = node["class_type"]
if not isinstance(class_type, str):
raise AssertionError("Generated class_type must be text.")
counts[class_type] = counts.get(class_type, 0) + 1
return counts
def _sampler_inputs(prompt: dict[str, JsonObject]) -> JsonObject:
"""Return the sole full attention-coupling sampler inputs."""
samplers = [
node["inputs"]
for node in prompt.values()
if node["class_type"] == "SimpleSyrup.KSamplerAttentionCoupling"
]
if len(samplers) != 1 or not isinstance(samplers[0], dict):
raise AssertionError("Expected one full attention-coupling sampler.")
return samplers[0]
def _sampler_seeds(prompt: dict[str, JsonObject]) -> set[int]:
"""Return every explicitly emitted SimpleSyrup sampler seed."""
seeds: set[int] = set()
for node in prompt.values():
class_type = node["class_type"]
if not isinstance(class_type, str) or not class_type.startswith(
"SimpleSyrup.KSampler"
):
continue
inputs = node["inputs"]
if not isinstance(inputs, dict):
raise AssertionError("Sampler inputs must be a JSON object.")
seed = inputs.get("seed")
if not isinstance(seed, int):
raise AssertionError("Sampler seed must be an integer.")
seeds.add(seed)
return seeds