2242 lines
104 KiB
Python
2242 lines
104 KiB
Python
import json
|
||
import base64
|
||
from io import BytesIO
|
||
from pathlib import Path
|
||
import sys
|
||
import types
|
||
|
||
import numpy as np
|
||
from PIL import Image, ImageDraw
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
sys.path.insert(0, str(REPO_ROOT))
|
||
|
||
|
||
def install_comfy_stubs():
|
||
for name in ["cv2", "node_helpers", "latent_preview"]:
|
||
sys.modules[name] = types.ModuleType(name)
|
||
|
||
folder_paths = types.ModuleType("folder_paths")
|
||
folder_paths.get_input_directory = lambda: "."
|
||
folder_paths.get_output_directory = lambda: "."
|
||
folder_paths.filter_files_content_types = lambda files, _types: files
|
||
folder_paths.get_annotated_filepath = lambda image: image
|
||
folder_paths.exists_annotated_filepath = lambda image: True
|
||
folder_paths.get_save_image_path = lambda filename_prefix, output_dir, image_width=0, image_height=0: (
|
||
".",
|
||
filename_prefix,
|
||
1,
|
||
"",
|
||
filename_prefix,
|
||
)
|
||
sys.modules["folder_paths"] = folder_paths
|
||
|
||
torch = types.ModuleType("torch")
|
||
|
||
class Tensor:
|
||
pass
|
||
|
||
class FakeTensor:
|
||
def __init__(self, array):
|
||
self.array = array
|
||
self.shape = getattr(array, "shape", ())
|
||
|
||
def unsqueeze(self, dim):
|
||
if dim == 0:
|
||
return FakeTensor(self.array[None, ...])
|
||
return self
|
||
|
||
def __getitem__(self, item):
|
||
return FakeTensor(self.array[item])
|
||
|
||
def cpu(self):
|
||
return self
|
||
|
||
def numpy(self):
|
||
return self.array
|
||
|
||
torch.Tensor = Tensor
|
||
torch.from_numpy = lambda array: FakeTensor(array)
|
||
torch.cat = lambda tensors, dim=0: tensors
|
||
torch.unsqueeze = lambda tensor, dim: tensor
|
||
sys.modules["torch"] = torch
|
||
|
||
comfy = types.ModuleType("comfy")
|
||
comfy.__path__ = []
|
||
sys.modules["comfy"] = comfy
|
||
for name in ["model_management", "sd", "samplers", "sample", "utils"]:
|
||
module = types.ModuleType(f"comfy.{name}")
|
||
sys.modules[f"comfy.{name}"] = module
|
||
setattr(comfy, name, module)
|
||
cli_args = types.ModuleType("comfy.cli_args")
|
||
cli_args.args = types.SimpleNamespace(disable_metadata=False)
|
||
sys.modules["comfy.cli_args"] = cli_args
|
||
|
||
|
||
def assert_equal(actual, expected, message):
|
||
if actual != expected:
|
||
raise AssertionError(f"{message}: expected {expected}, got {actual}")
|
||
|
||
|
||
def assert_image_close(actual, expected, message, tolerance=1e-6):
|
||
actual_array = getattr(actual, "array", actual)
|
||
expected_array = getattr(expected, "array", expected)
|
||
if not np.allclose(actual_array, expected_array, atol=tolerance, rtol=0.0):
|
||
max_delta = float(np.max(np.abs(actual_array - expected_array)))
|
||
raise AssertionError(f"{message}: max delta {max_delta} > {tolerance}")
|
||
|
||
|
||
def call_assemble_like_comfy(node, widget_values, **linked_inputs):
|
||
kwargs = {}
|
||
required = node.INPUT_TYPES()["required"]
|
||
for index, (name, spec) in enumerate(required.items()):
|
||
if index < len(widget_values):
|
||
kwargs[name] = widget_values[index]
|
||
continue
|
||
if isinstance(spec, tuple) and len(spec) > 1 and isinstance(spec[1], dict) and "default" in spec[1]:
|
||
kwargs[name] = spec[1]["default"]
|
||
kwargs.update(linked_inputs)
|
||
return node.assemble_tiles(**kwargs)
|
||
|
||
|
||
install_comfy_stubs()
|
||
|
||
import TTP_toolsets as ttp # noqa: E402
|
||
|
||
|
||
assert_equal(
|
||
"TTP_Smart_Tile_Layout_Experimental" in ttp.NODE_CLASS_MAPPINGS,
|
||
False,
|
||
"old Smart Tile layout step node should not be registered",
|
||
)
|
||
assert_equal(
|
||
"TTP_Smart_Tile_Crop_Experimental" in ttp.NODE_CLASS_MAPPINGS,
|
||
False,
|
||
"old Smart Tile crop step node should not be registered",
|
||
)
|
||
assert_equal(
|
||
"TTP_Smart_Tile_Visual_Crop_Experimental" in ttp.NODE_CLASS_MAPPINGS,
|
||
False,
|
||
"old Smart Tile param crop step node should not be registered",
|
||
)
|
||
assert_equal(
|
||
"(Experimental)" in ttp.NODE_DISPLAY_NAME_MAPPINGS["TTP_Smart_Tile_Interactive_Crop_Experimental"],
|
||
False,
|
||
"interactive crop display name should not include Experimental",
|
||
)
|
||
assert_equal(
|
||
ttp.NODE_DISPLAY_NAME_MAPPINGS["TTP_Smart_Tile_Loop_Source_Experimental"],
|
||
"TTP Smart Tile Loop Source",
|
||
"loop workflow display names should be stable and non-experimental",
|
||
)
|
||
assert_equal(
|
||
ttp.TTP_Smart_Tile_Interactive_Crop_Experimental.CATEGORY,
|
||
"TTP/Smart Tile",
|
||
"Smart Tile nodes should use the non-experimental menu category",
|
||
)
|
||
|
||
interactive_inputs = ttp.TTP_Smart_Tile_Interactive_Crop_Experimental.INPUT_TYPES()
|
||
required_inputs = interactive_inputs["required"]
|
||
optional_inputs = interactive_inputs["optional"]
|
||
hidden_inputs = interactive_inputs["hidden"]
|
||
assert_equal(
|
||
ttp.TTP_Smart_Tile_Interactive_Crop_Experimental.RETURN_TYPES,
|
||
("IMAGE", "IMAGE", "TTP_SMART_TILE_SET", "TTP_SMART_TILE_META", "LIST", "IMAGE", "STRING"),
|
||
"interactive crop should expose a tile_set output before metadata",
|
||
)
|
||
assert_equal(
|
||
required_inputs["auto_detect_mode"][0],
|
||
["none", "sam3.1", "qwenvl3"],
|
||
"interactive crop should expose the requested auto detect modes",
|
||
)
|
||
assert_equal("auto_detect_request" in required_inputs, True, "interactive crop should expose an inference request trigger")
|
||
assert_equal("auto_prompt" in required_inputs, True, "interactive crop should expose a visual prompt")
|
||
assert_equal("allow_object_overlap" in required_inputs, True, "interactive crop should expose object overlap control")
|
||
assert_equal("auto_object_padding" in required_inputs, True, "interactive crop should expose object padding control")
|
||
assert_equal("auto_mask_expand" in required_inputs, True, "interactive crop should expose auto mask expansion control")
|
||
assert_equal("auto_max_tiles" in required_inputs, True, "interactive crop should expose auto max tiles control")
|
||
assert_equal("auto_paint_mask" in required_inputs, True, "interactive crop should expose hidden painted mask input")
|
||
assert_equal("vision_model" in optional_inputs, True, "interactive crop should expose a vision model input")
|
||
assert_equal("vision_conditioning" in optional_inputs, True, "interactive crop should expose official SAM3 conditioning input")
|
||
assert_equal("clip" in optional_inputs, True, "interactive crop should optionally encode SAM3 prompts with CLIP")
|
||
assert_equal("qwen_vl_model" in optional_inputs, True, "interactive crop should accept local QwenVL model for bbox auto tile")
|
||
assert_equal(hidden_inputs["unique_id"], "UNIQUE_ID", "interactive crop should receive its ComfyUI node id")
|
||
|
||
assemble_inputs = ttp.TTP_Smart_Tile_Assemble_Experimental.INPUT_TYPES()
|
||
expected_assemble_required_order = [
|
||
"blend_multiplier",
|
||
"output_scale",
|
||
"use_priority",
|
||
"tile_alignment",
|
||
"edge_crop_px",
|
||
"color_correction",
|
||
"color_strength",
|
||
"mask_blend_mode",
|
||
"pixel_alignment",
|
||
"pixel_alignment_radius",
|
||
"pixel_alignment_device",
|
||
"large_tile_policy",
|
||
"large_tile_area_threshold",
|
||
"min_tile_scale_ratio",
|
||
"context_tile_weight",
|
||
"assemble_device",
|
||
"assemble_mode",
|
||
"base_canvas_mode",
|
||
"weight_preview_mode",
|
||
"small_tile_on_top",
|
||
"auto_composite_policy",
|
||
"replace_tile_shape",
|
||
]
|
||
assert_equal(
|
||
list(assemble_inputs["required"].keys()),
|
||
expected_assemble_required_order,
|
||
"assemble required widgets should keep the same order ComfyUI stores in workflow widget values",
|
||
)
|
||
assert_equal("sampled_tiles" in assemble_inputs["optional"], True, "assemble should keep batch tiles as an optional compatibility input")
|
||
assert_equal("tile_meta" in assemble_inputs["optional"], True, "assemble should keep batch metadata as an optional compatibility input")
|
||
assert_equal("tile_set" in assemble_inputs["optional"], True, "assemble should accept true variable-size tile sets")
|
||
assert_equal("local_mean_std" in assemble_inputs["required"]["color_correction"][0], True, "assemble should expose local mean/std color correction")
|
||
assert_equal("tile_alignment" in assemble_inputs["required"], True, "assemble should expose tile alignment")
|
||
assert_equal("edge_crop_px" in assemble_inputs["required"], True, "assemble should expose edge crop control")
|
||
assert_equal("mask_blend_mode" in assemble_inputs["required"], True, "assemble should expose object mask blending")
|
||
assert_equal("pixel_alignment" in assemble_inputs["required"], True, "assemble should expose pixel alignment")
|
||
assert_equal("pixel_alignment_radius" in assemble_inputs["required"], True, "assemble should expose pixel alignment radius")
|
||
assert_equal(assemble_inputs["required"]["pixel_alignment_device"][0], ["auto", "cpu", "gpu"], "assemble should expose CPU/GPU pixel alignment selection")
|
||
assert_equal(assemble_inputs["required"]["assemble_device"][0], ["auto", "cpu", "gpu"], "assemble should expose CPU/GPU paste selection")
|
||
assert_equal(assemble_inputs["required"]["assemble_mode"][0], ["final_only", "always"], "assemble should expose final-only mode first")
|
||
assert_equal(assemble_inputs["required"]["assemble_mode"][1]["default"], "final_only", "assemble should default to final-only loop compositing")
|
||
assert_equal(assemble_inputs["required"]["base_canvas_mode"][0], ["auto", "black", "base_image", "source_image"], "assemble should expose base canvas source selection")
|
||
assert_equal(assemble_inputs["required"]["weight_preview_mode"][0], ["raw_weight", "coverage"], "assemble should expose raw weight and coverage preview modes")
|
||
assert_equal(assemble_inputs["required"]["weight_preview_mode"][1]["default"], "raw_weight", "assemble should keep raw overlap weight preview as the default")
|
||
assert_equal("small_tile_on_top" in assemble_inputs["required"], True, "assemble should expose small tile top stacking")
|
||
assert_equal(assemble_inputs["required"]["auto_composite_policy"][0], ["safe_auto", "strict_layer", "soft_detail", "replace_object", "replace_tile"], "assemble should expose automatic compositing policy")
|
||
assert_equal(assemble_inputs["required"]["auto_composite_policy"][1]["default"], "safe_auto", "assemble should default to safe auto compositing")
|
||
assert_equal(assemble_inputs["required"]["replace_tile_shape"][0], ["mask_first", "tile_box_first"], "assemble should expose replace tile shape priority")
|
||
assert_equal(assemble_inputs["required"]["replace_tile_shape"][1]["default"], "mask_first", "assemble should default replace tile to mask priority")
|
||
assert_equal(assemble_inputs["optional"]["done"][0], "BOOLEAN", "assemble should accept loop done gate")
|
||
preview_inputs = ttp.TTP_Smart_Tile_Set_Preview_Experimental.INPUT_TYPES()
|
||
assert_equal(preview_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "tile set preview should accept Smart Tile Set")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Set_Preview_Experimental"],
|
||
ttp.TTP_Smart_Tile_Set_Preview_Experimental,
|
||
"tile set preview should be registered",
|
||
)
|
||
prompt_builder_inputs = ttp.TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental.INPUT_TYPES()
|
||
assert_equal("tile_set" in prompt_builder_inputs["required"], False, "prompt builder should not put forceInput tile_set before visible widgets")
|
||
assert_equal(prompt_builder_inputs["optional"]["tile_set"][1].get("forceInput"), True, "prompt builder tile_set should be an optional forceInput connection")
|
||
assert_equal(prompt_builder_inputs["required"]["reference_image_mode"][0], ["none", "first_message", "every_tile", "contact_sheet"], "prompt builder should support reference image and contact sheet strategies")
|
||
assert_equal("mode" in prompt_builder_inputs["required"], False, "prompt builder should auto-select template or local QwenVL mode from the model input")
|
||
assert_equal("prompt_preset" in prompt_builder_inputs["required"], True, "prompt builder should expose preset prompt selection")
|
||
assert_equal("qwen_max_side" in prompt_builder_inputs["required"], True, "prompt builder should expose Qwen image resize control")
|
||
assert_equal("use_tile_cache" in prompt_builder_inputs["required"], True, "prompt builder should expose tile-level cache control")
|
||
assert_equal("endpoint_url" in prompt_builder_inputs["required"], False, "prompt builder should not expose API endpoint fields")
|
||
assert_equal("model_name" in prompt_builder_inputs["required"], False, "prompt builder should use the connected local QwenVL model instead of a model-name widget")
|
||
prompt_required_order = list(prompt_builder_inputs["required"].keys())
|
||
assert_equal(
|
||
prompt_required_order.index("prompt_preset") > prompt_required_order.index("temperature"),
|
||
True,
|
||
"new prompt builder widgets should be appended after legacy prompt fields to avoid saved-widget value shifts",
|
||
)
|
||
assert_equal(
|
||
prompt_required_order.index("global_negative") > prompt_required_order.index("use_tile_cache"),
|
||
True,
|
||
"global negative should stay at the end to avoid shifting core prompt widgets",
|
||
)
|
||
assert_equal(prompt_builder_inputs["required"]["system_prompt"][1]["dynamicPrompts"], False, "system prompt should not use ComfyUI dynamic prompt parsing")
|
||
assert_equal(prompt_builder_inputs["required"]["tile_instruction"][1]["dynamicPrompts"], False, "tile instruction should not use ComfyUI dynamic prompt parsing")
|
||
assert_equal("seed" in prompt_builder_inputs["required"], False, "prompt builder should avoid ComfyUI's special seed widget name")
|
||
assert_equal("seed" in prompt_builder_inputs["optional"], False, "prompt builder should not expose the legacy seed widget name")
|
||
assert_equal(prompt_builder_inputs["required"]["qwen_seed"][0], "INT", "prompt builder should expose qwen_seed for local QwenVL sampling")
|
||
assert_equal(prompt_builder_inputs["optional"]["qwen_vl_model"][0], "TTP_QWENVL3_MODEL", "prompt builder should accept local QwenVL loader output")
|
||
qwen_loader_inputs = ttp.TTP_QwenVL3_Local_Loader_Experimental.INPUT_TYPES()
|
||
assert_equal("model_file" in qwen_loader_inputs["required"], True, "QwenVL local loader should expose a safetensors file picker")
|
||
assert_equal(qwen_loader_inputs["required"]["model_family"][0], ["auto", "qwen_vl"], "QwenVL local loader should focus on visual tagging model family")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_QwenVL3_Local_Loader_Experimental"],
|
||
ttp.TTP_QwenVL3_Local_Loader_Experimental,
|
||
"QwenVL local loader should be registered",
|
||
)
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental"],
|
||
ttp.TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental,
|
||
"prompt builder should be registered",
|
||
)
|
||
prompt_override_inputs = ttp.TTP_Smart_Tile_Prompt_Override_Experimental.INPUT_TYPES()
|
||
assert_equal(prompt_override_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "prompt override should accept Smart Tile Set")
|
||
assert_equal(prompt_override_inputs["required"]["tile_set"][1].get("forceInput"), True, "prompt override tile_set should be a forceInput connection")
|
||
assert_equal("regex" in prompt_override_inputs["required"]["selector_type"][0], True, "prompt override should support regex selectors")
|
||
assert_equal(prompt_override_inputs["required"]["unmatched_mode"][0], ["keep", "drop_unmatched"], "prompt override should allow dropping unmatched tiles")
|
||
assert_equal(prompt_override_inputs["required"]["prompt_text"][1]["dynamicPrompts"], False, "prompt override text should not use dynamic prompt parsing")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Prompt_Override_Experimental"],
|
||
ttp.TTP_Smart_Tile_Prompt_Override_Experimental,
|
||
"prompt override should be registered",
|
||
)
|
||
|
||
override_tile_set = {
|
||
"version": 1,
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [100, 100],
|
||
"tile_meta": {
|
||
"version": 3,
|
||
"type": "ttp_smart_tile",
|
||
"storage": "tile_set",
|
||
"original_size": [100, 100],
|
||
"tiles": [
|
||
{
|
||
"id": 0,
|
||
"name": "tile_1",
|
||
"label": "face",
|
||
"source": "paint_mask",
|
||
"core_box": [0, 0, 50, 50],
|
||
"sample_box": [0, 0, 50, 50],
|
||
"paste_box": [0, 0, 50, 50],
|
||
"prompt": "old face prompt",
|
||
"negative": "old face negative",
|
||
},
|
||
{
|
||
"id": 1,
|
||
"name": "tile_2",
|
||
"label": "background",
|
||
"source": "manual",
|
||
"core_box": [50, 0, 50, 50],
|
||
"sample_box": [50, 0, 50, 50],
|
||
"paste_box": [50, 0, 50, 50],
|
||
"prompt": "old background prompt",
|
||
"negative": "old background negative",
|
||
},
|
||
],
|
||
},
|
||
"tile_images": ["face_image", "background_image"],
|
||
"positions": [(0, 0, 50, 50), (50, 0, 100, 50)],
|
||
}
|
||
override_node = ttp.TTP_Smart_Tile_Prompt_Override_Experimental()
|
||
overridden_set, override_json, override_summary = override_node.override_prompts(
|
||
override_tile_set,
|
||
selector_type="label",
|
||
selector="face",
|
||
unmatched_mode="keep",
|
||
prompt_mode="replace",
|
||
prompt_text="edit only the face",
|
||
negative_mode="append",
|
||
negative_text="avoid changing identity",
|
||
)
|
||
assert_equal(overridden_set["tile_meta"]["tiles"][0]["prompt"], "edit only the face", "prompt override should replace matched tile prompt")
|
||
assert_equal(overridden_set["tile_meta"]["tiles"][0]["negative"], "old face negative\navoid changing identity", "prompt override should append matched tile negative")
|
||
assert_equal(overridden_set["tile_meta"]["tiles"][1]["prompt"], "old background prompt", "prompt override should keep unmatched tile prompt")
|
||
assert_equal(json.loads(override_json)["matched"], 1, "prompt override JSON should report matched tile count")
|
||
assert_equal("matched=1" in override_summary, True, "prompt override summary should report matched count")
|
||
dropped_set, _dropped_json, _dropped_summary = override_node.override_prompts(
|
||
override_tile_set,
|
||
selector_type="label",
|
||
selector="face",
|
||
unmatched_mode="drop_unmatched",
|
||
prompt_mode="replace",
|
||
prompt_text="edit only the face",
|
||
)
|
||
assert_equal(len(dropped_set["tile_meta"]["tiles"]), 1, "prompt override should drop unmatched tile metadata")
|
||
assert_equal(len(dropped_set["tile_images"]), 1, "prompt override should drop unmatched tile images")
|
||
assert_equal(dropped_set["tile_meta"]["tiles"][0]["id"], 0, "prompt override should reindex kept tile ids")
|
||
composite_override_inputs = ttp.TTP_Smart_Tile_Composite_Override_Experimental.INPUT_TYPES()
|
||
assert_equal(composite_override_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "composite override should accept Smart Tile Set")
|
||
assert_equal(composite_override_inputs["required"]["tile_set"][1].get("forceInput"), True, "composite override tile_set should be a forceInput connection")
|
||
assert_equal("replace_tile" in composite_override_inputs["required"]["composite_mode"][0], True, "composite override should expose replace_tile mode")
|
||
assert_equal(composite_override_inputs["required"]["replace_tile_shape"][0], ["keep", "mask_first", "tile_box_first"], "composite override should expose replace tile shape priority")
|
||
assert_equal(composite_override_inputs["required"]["selector_type"][0], prompt_override_inputs["required"]["selector_type"][0], "composite override selectors should match prompt override selectors")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Composite_Override_Experimental"],
|
||
ttp.TTP_Smart_Tile_Composite_Override_Experimental,
|
||
"composite override should be registered",
|
||
)
|
||
composite_node = ttp.TTP_Smart_Tile_Composite_Override_Experimental()
|
||
composited_set, composite_json, composite_summary = composite_node.override_composite(
|
||
override_tile_set,
|
||
selector_type="label",
|
||
selector="face",
|
||
composite_mode="replace_tile",
|
||
replace_tile_shape="tile_box_first",
|
||
layer_mode="max",
|
||
layer=12,
|
||
priority_mode="max",
|
||
priority=220,
|
||
occlusion_priority_mode="max",
|
||
occlusion_priority=60000,
|
||
blend_mode="set",
|
||
blend=24,
|
||
)
|
||
composited_tile = composited_set["tile_meta"]["tiles"][0]
|
||
assert_equal(composited_tile["composite_mode"], "replace_tile", "composite override should set replace_tile mode")
|
||
assert_equal(composited_tile["recommended_composite_mode"], "replace_tile", "composite override should update recommended composite mode")
|
||
assert_equal(composited_tile["replace_tile_shape"], "tile_box_first", "composite override should set replace tile shape priority")
|
||
assert_equal(composited_tile["layer"], 12, "composite override should raise the selected tile layer")
|
||
assert_equal(composited_tile["priority"], 220, "composite override should raise the selected tile priority")
|
||
assert_equal(composited_tile["occlusion_priority"], 60000, "composite override should raise the selected tile occlusion priority")
|
||
assert_equal(composited_tile["blend"], 24, "composite override should set selected tile blend")
|
||
assert_equal(json.loads(composite_json)["matched"], 1, "composite override JSON should report matched tile count")
|
||
assert_equal("matched=1" in composite_summary, True, "composite override summary should report matched count")
|
||
stack_order_inputs = ttp.TTP_Smart_Tile_Stack_Order_Experimental.INPUT_TYPES()
|
||
assert_equal(stack_order_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "stack order should accept Smart Tile Set")
|
||
assert_equal(stack_order_inputs["required"]["tile_set"][1].get("forceInput"), True, "stack order tile_set should be a forceInput connection")
|
||
assert_equal(stack_order_inputs["required"]["top_order"][0], ["first_on_top", "last_on_top"], "stack order should expose top order direction")
|
||
assert_equal(stack_order_inputs["required"]["composite_mode"][1]["default"], "replace_tile", "stack order should default to replace-tile compositing")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Stack_Order_Experimental"],
|
||
ttp.TTP_Smart_Tile_Stack_Order_Experimental,
|
||
"stack order should be registered",
|
||
)
|
||
stack_order_tile_set = {
|
||
"version": 1,
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [100, 100],
|
||
"tile_meta": {
|
||
"version": 3,
|
||
"type": "ttp_smart_tile",
|
||
"storage": "tile_set",
|
||
"original_size": [100, 100],
|
||
"tiles": [
|
||
{"id": 0, "name": "tile_1", "core_box": [0, 0, 50, 50], "sample_box": [0, 0, 50, 50], "layer": 0, "occlusion_priority": 0},
|
||
{"id": 1, "name": "tile_2", "core_box": [0, 0, 50, 50], "sample_box": [0, 0, 50, 50], "layer": 0, "occlusion_priority": 0},
|
||
{"id": 2, "name": "tile_3", "core_box": [0, 0, 50, 50], "sample_box": [0, 0, 50, 50], "layer": 0, "occlusion_priority": 0},
|
||
{"id": 3, "name": "tile_4", "core_box": [0, 0, 50, 50], "sample_box": [0, 0, 50, 50], "layer": 1, "occlusion_priority": 80000},
|
||
],
|
||
},
|
||
"tile_images": ["tile_1", "tile_2", "tile_3", "tile_4"],
|
||
}
|
||
stack_node = ttp.TTP_Smart_Tile_Stack_Order_Experimental()
|
||
stacked_set, stack_json, stack_summary = stack_node.apply_stack_order(
|
||
stack_order_tile_set,
|
||
tile_order="T1,T3,T2",
|
||
)
|
||
stacked_tiles = stacked_set["tile_meta"]["tiles"]
|
||
assert_equal(stacked_tiles[0]["occlusion_priority"] > stacked_tiles[2]["occlusion_priority"], True, "stack order should make the first listed tile topmost by default")
|
||
assert_equal(stacked_tiles[2]["occlusion_priority"] > stacked_tiles[1]["occlusion_priority"], True, "stack order should preserve listed ordering between selected tiles")
|
||
assert_equal(stacked_tiles[1]["occlusion_priority"] > stacked_tiles[3]["occlusion_priority"], True, "stack order should place listed tiles above unlisted tiles")
|
||
assert_equal(stacked_tiles[0]["composite_mode"], "replace_tile", "stack order should default selected tiles to replace_tile mode")
|
||
assert_equal("composite_mode" in stacked_tiles[3], False, "stack order should not change unlisted tile composite mode")
|
||
assert_equal(json.loads(stack_json)["tiles"][1]["selector"], "T3", "stack order JSON should preserve selector order")
|
||
assert_equal("T1" in stack_summary and "T3" in stack_summary and "T2" in stack_summary, True, "stack order summary should include selected tiles")
|
||
loop_source_inputs = ttp.TTP_Smart_Tile_Loop_Source_Experimental.INPUT_TYPES()
|
||
assert_equal(loop_source_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "loop source should accept Smart Tile Set")
|
||
assert_equal(loop_source_inputs["optional"]["clip"][0], "CLIP", "loop source should optionally encode tile prompts with CLIP")
|
||
assert_equal(loop_source_inputs["hidden"]["unique_id"], "UNIQUE_ID", "loop source should know its node id for auto queue events")
|
||
assert_equal("prompt" in ttp.TTP_Smart_Tile_Loop_Source_Experimental.RETURN_NAMES, True, "loop source should output tile prompts")
|
||
assert_equal("negative" in ttp.TTP_Smart_Tile_Loop_Source_Experimental.RETURN_NAMES, True, "loop source should output tile negatives")
|
||
assert_equal("positive_conditioning" in ttp.TTP_Smart_Tile_Loop_Source_Experimental.RETURN_NAMES, True, "loop source should output encoded positive conditioning")
|
||
assert_equal("negative_conditioning" in ttp.TTP_Smart_Tile_Loop_Source_Experimental.RETURN_NAMES, True, "loop source should output encoded negative conditioning")
|
||
loop_collect_inputs = ttp.TTP_Smart_Tile_Loop_Collect_Experimental.INPUT_TYPES()
|
||
assert_equal(loop_collect_inputs["required"]["tile_task"][0], "TTP_SMART_TILE_TASK", "loop collect should accept tile tasks")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Loop_Source_Experimental"],
|
||
ttp.TTP_Smart_Tile_Loop_Source_Experimental,
|
||
"loop source should be registered",
|
||
)
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Loop_Collect_Experimental"],
|
||
ttp.TTP_Smart_Tile_Loop_Collect_Experimental,
|
||
"loop collect should be registered",
|
||
)
|
||
upscale_inputs = ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental.INPUT_TYPES()
|
||
assert_equal(upscale_inputs["required"]["image"][0], "IMAGE", "upscale prep should accept one tile image")
|
||
assert_equal("max_megapixels" in upscale_inputs["required"], True, "upscale prep should expose a megapixel cap")
|
||
assert_equal("use_upscale_model" in upscale_inputs["required"], True, "upscale prep should allow disabling a connected upscale model")
|
||
assert_equal(upscale_inputs["optional"]["upscale_model"][0], "UPSCALE_MODEL", "upscale prep should accept ComfyUI upscale models")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Image_Upscale_Prep_Experimental"],
|
||
ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental,
|
||
"upscale prep should be registered",
|
||
)
|
||
output_size_inputs = ttp.TTP_Smart_Tile_Output_Size_Estimate_Experimental.INPUT_TYPES()
|
||
assert_equal(output_size_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "output size estimate should accept tile sets")
|
||
assert_equal(output_size_inputs["required"]["scale_strategy"][0], ["median", "mean", "min", "max", "focus_weighted"], "output size estimate should expose scale strategies")
|
||
assert_equal(output_size_inputs["optional"]["done"][0], "BOOLEAN", "output size estimate should accept a loop done gate")
|
||
semantic_rank_inputs = ttp.TTP_Smart_Tile_Semantic_Rank_Experimental.INPUT_TYPES()
|
||
assert_equal(semantic_rank_inputs["required"]["tile_set"][0], "TTP_SMART_TILE_SET", "semantic rank should accept Smart Tile Set")
|
||
assert_equal(semantic_rank_inputs["required"]["rank_policy"][0], ["portrait", "balanced", "product", "text"], "semantic rank should expose rank policies")
|
||
assert_equal("apply_composite_rank" in semantic_rank_inputs["required"], True, "semantic rank should optionally write composite rank metadata")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Semantic_Rank_Experimental"],
|
||
ttp.TTP_Smart_Tile_Semantic_Rank_Experimental,
|
||
"semantic rank should be registered",
|
||
)
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Output_Size_Estimate_Experimental"],
|
||
ttp.TTP_Smart_Tile_Output_Size_Estimate_Experimental,
|
||
"output size estimate should be registered",
|
||
)
|
||
save_final_inputs = ttp.TTP_Smart_Tile_Save_Final_Image_Experimental.INPUT_TYPES()
|
||
assert_equal(save_final_inputs["required"]["images"][0], "IMAGE", "save final image should accept images")
|
||
assert_equal("done" in save_final_inputs["required"], True, "save final image should expose a done gate")
|
||
assert_equal(save_final_inputs["hidden"]["extra_pnginfo"], "EXTRA_PNGINFO", "save final image should embed workflow metadata")
|
||
assert_equal(
|
||
ttp.NODE_CLASS_MAPPINGS["TTP_Smart_Tile_Save_Final_Image_Experimental"],
|
||
ttp.TTP_Smart_Tile_Save_Final_Image_Experimental,
|
||
"save final image should be registered",
|
||
)
|
||
|
||
|
||
# This mirrors JSON.stringify output from the browser: exact 0/1 edges are
|
||
# serialized as integers, while inner grid lines stay fractional.
|
||
browser_grid_layout = {
|
||
"tiles": [
|
||
{
|
||
"name": f"tile_{row}_{col}",
|
||
"x0": 0 if col == 0 else col / 3,
|
||
"y0": 0 if row == 0 else row / 3,
|
||
"x1": 1 if col == 2 else (col + 1) / 3,
|
||
"y1": 1 if row == 2 else (row + 1) / 3,
|
||
}
|
||
for row in range(3)
|
||
for col in range(3)
|
||
]
|
||
}
|
||
|
||
layout_json = json.dumps(browser_grid_layout, separators=(",", ":"))
|
||
normalized_layout = ttp._ttp_interactive_layout_with_defaults(layout_json, 64, 32, False)
|
||
tiles_meta = ttp._ttp_parse_smart_tile_layout(normalized_layout, 900, 600)
|
||
|
||
expected_core_boxes = [
|
||
[0, 0, 300, 200],
|
||
[300, 0, 300, 200],
|
||
[600, 0, 300, 200],
|
||
[0, 200, 300, 200],
|
||
[300, 200, 300, 200],
|
||
[600, 200, 300, 200],
|
||
[0, 400, 300, 200],
|
||
[300, 400, 300, 200],
|
||
[600, 400, 300, 200],
|
||
]
|
||
|
||
assert_equal([tile["core_box"] for tile in tiles_meta], expected_core_boxes, "3x3 core boxes")
|
||
|
||
expected_sample_boxes = [
|
||
[0, 0, 364, 264],
|
||
[236, 0, 428, 264],
|
||
[536, 0, 364, 264],
|
||
[0, 136, 364, 328],
|
||
[236, 136, 428, 328],
|
||
[536, 136, 364, 328],
|
||
[0, 336, 364, 264],
|
||
[236, 336, 428, 264],
|
||
[536, 336, 364, 264],
|
||
]
|
||
|
||
assert_equal([tile["sample_box"] for tile in tiles_meta], expected_sample_boxes, "3x3 overlap sample boxes")
|
||
|
||
_tiles, crop_meta, _positions, _preview = ttp._ttp_crop_smart_tiles_from_meta(
|
||
Image.new("RGB", (900, 600), "white"),
|
||
[dict(tile) for tile in tiles_meta],
|
||
8,
|
||
)
|
||
expected_batched_sample_boxes = [
|
||
[0, 0, 432, 328],
|
||
[236, 0, 432, 328],
|
||
[468, 0, 432, 328],
|
||
[0, 136, 432, 328],
|
||
[236, 136, 432, 328],
|
||
[468, 136, 432, 328],
|
||
[0, 272, 432, 328],
|
||
[236, 272, 432, 328],
|
||
[468, 272, 432, 328],
|
||
]
|
||
assert_equal(
|
||
[tile["sample_box"] for tile in crop_meta["tiles"]],
|
||
expected_batched_sample_boxes,
|
||
"3x3 batch crops should expand inward with real image pixels instead of transport padding",
|
||
)
|
||
assert_equal(
|
||
crop_meta["tiles"][0]["tile_canvas_box"],
|
||
[0, 0, 432, 328],
|
||
"top-left crop should fill the batch canvas with real pixels",
|
||
)
|
||
assert_equal(
|
||
crop_meta["tiles"][0]["tile_canvas_size"],
|
||
[432, 328],
|
||
"top-left batch canvas should match largest crop",
|
||
)
|
||
assert_equal(
|
||
crop_meta["tiles"][8]["tile_canvas_box"],
|
||
[0, 0, 432, 328],
|
||
"bottom-right crop should fill the batch canvas with real pixels",
|
||
)
|
||
assert_equal(
|
||
crop_meta["tiles"][0]["overlap_edges_px_source"],
|
||
{"left": 0, "right": 132, "top": 0, "bottom": 128},
|
||
"top-left batch crop should grow only inward",
|
||
)
|
||
assert_equal(
|
||
crop_meta["tiles"][8]["overlap_edges_px_source"],
|
||
{"left": 132, "right": 0, "top": 128, "bottom": 0},
|
||
"bottom-right batch crop should grow only inward",
|
||
)
|
||
|
||
tile_set = ttp._ttp_crop_smart_tile_set_from_meta(
|
||
Image.new("RGB", (900, 600), "white"),
|
||
[dict(tile) for tile in tiles_meta],
|
||
8,
|
||
)
|
||
expected_tile_set_sample_boxes = [
|
||
[0, 0, 368, 264],
|
||
[236, 0, 432, 264],
|
||
[532, 0, 368, 264],
|
||
[0, 136, 368, 328],
|
||
[236, 136, 432, 328],
|
||
[532, 136, 368, 328],
|
||
[0, 336, 368, 264],
|
||
[236, 336, 432, 264],
|
||
[532, 336, 368, 264],
|
||
]
|
||
assert_equal(tile_set["type"], "ttp_smart_tile_set", "tile_set should use the custom Smart Tile Set type")
|
||
assert_equal(
|
||
[tile["sample_box"] for tile in tile_set["tile_meta"]["tiles"]],
|
||
expected_tile_set_sample_boxes,
|
||
"tile_set crops should keep per-tile sample boxes after round_to alignment",
|
||
)
|
||
assert_equal(
|
||
[tile["tile_canvas_size"] for tile in tile_set["tile_meta"]["tiles"]],
|
||
[[box[2], box[3]] for box in expected_tile_set_sample_boxes],
|
||
"tile_set canvas sizes should stay per tile instead of batch-sized",
|
||
)
|
||
assert_equal(
|
||
[list(image.shape[1:3]) for image in tile_set["tile_images"]],
|
||
[[box[3], box[2]] for box in expected_tile_set_sample_boxes],
|
||
"tile_set image tensors should preserve different tile dimensions",
|
||
)
|
||
preview_node = ttp.TTP_Smart_Tile_Set_Preview_Experimental()
|
||
contact_sheet, sheet_info = preview_node.preview_tile_set(
|
||
tile_set,
|
||
mode="contact_sheet",
|
||
thumbnail_size=128,
|
||
columns=3,
|
||
)
|
||
assert_equal(contact_sheet.shape[0], 1, "tile set contact sheet should output a regular IMAGE tensor")
|
||
assert_equal("368x264" in sheet_info and "432x328" in sheet_info, True, "tile set preview info should report real tile sizes")
|
||
selected_tile, selected_info = preview_node.preview_tile_set(
|
||
tile_set,
|
||
mode="selected_tile",
|
||
selected_index=4,
|
||
)
|
||
assert_equal(list(selected_tile.shape[1:3]), [328, 432], "selected tile preview should preserve the selected tile size")
|
||
assert_equal("4:" in selected_info, True, "selected tile preview should include tile index info")
|
||
|
||
grid_assemble_node = ttp.TTP_Smart_Tile_Assemble_Experimental()
|
||
_grid_output_raw, grid_raw_weight_preview = grid_assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
tile_set=tile_set,
|
||
)
|
||
assert_equal(float(grid_raw_weight_preview.array.max()), 1.0, "raw 3x3 weight preview should normalize the highest overlap to white")
|
||
assert_equal(float(grid_raw_weight_preview.array.min()) < 1.0, True, "raw 3x3 weight preview should show lower-weight single tile areas darker than overlap zones")
|
||
_grid_output, grid_coverage_preview = grid_assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
weight_preview_mode="coverage",
|
||
mask_blend_mode="mask_feather",
|
||
tile_set=tile_set,
|
||
)
|
||
assert_equal(float(grid_coverage_preview.array.min()), 1.0, "black canvas 3x3 coverage preview should be uniformly covered")
|
||
assert_equal(float(grid_coverage_preview.array.max()), 1.0, "black canvas 3x3 coverage preview should not show raw overlap weight grid lines")
|
||
|
||
comfy_assemble_widget_values = [
|
||
1.0,
|
||
0.0,
|
||
True,
|
||
"resize",
|
||
0,
|
||
"off",
|
||
0.35,
|
||
"mask_feather",
|
||
"off",
|
||
0,
|
||
"cpu",
|
||
"use_if_higher_resolution",
|
||
0.55,
|
||
0.95,
|
||
0.25,
|
||
"cpu",
|
||
"final_only",
|
||
"black",
|
||
"raw_weight",
|
||
False,
|
||
"safe_auto",
|
||
]
|
||
direct_comfy_output, direct_comfy_weights = grid_assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
tile_alignment="resize",
|
||
edge_crop_px=0,
|
||
color_correction="off",
|
||
color_strength=0.35,
|
||
mask_blend_mode="mask_feather",
|
||
pixel_alignment="off",
|
||
pixel_alignment_radius=0,
|
||
pixel_alignment_device="cpu",
|
||
large_tile_policy="use_if_higher_resolution",
|
||
large_tile_area_threshold=0.55,
|
||
min_tile_scale_ratio=0.95,
|
||
context_tile_weight=0.25,
|
||
assemble_device="cpu",
|
||
assemble_mode="final_only",
|
||
base_canvas_mode="black",
|
||
weight_preview_mode="raw_weight",
|
||
small_tile_on_top=False,
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=tile_set,
|
||
done=True,
|
||
)
|
||
replayed_comfy_output, replayed_comfy_weights = call_assemble_like_comfy(
|
||
grid_assemble_node,
|
||
comfy_assemble_widget_values,
|
||
output_scale=1.0,
|
||
tile_set=tile_set,
|
||
done=True,
|
||
)
|
||
assert_image_close(replayed_comfy_output, direct_comfy_output, "ComfyUI widget-order assemble replay should match direct assemble output")
|
||
assert_image_close(replayed_comfy_weights, direct_comfy_weights, "ComfyUI widget-order assemble replay should match direct assemble weight preview")
|
||
|
||
ranked_grid_meta = dict(tile_set["tile_meta"])
|
||
ranked_grid_tiles = []
|
||
for tile_index, tile in enumerate(tile_set["tile_meta"]["tiles"]):
|
||
ranked_tile = dict(tile)
|
||
ranked_tile["layer"] = tile_index
|
||
ranked_tile["occlusion_priority"] = tile_index
|
||
ranked_grid_tiles.append(ranked_tile)
|
||
ranked_grid_meta["tiles"] = ranked_grid_tiles
|
||
ranked_grid_tile_set = dict(tile_set)
|
||
ranked_grid_tile_set["tile_meta"] = ranked_grid_meta
|
||
_ranked_grid_output, ranked_grid_weight_preview = grid_assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=ranked_grid_tile_set,
|
||
)
|
||
ranked_width, ranked_height = ranked_grid_meta["original_size"]
|
||
internal_x = sorted({tile["core_box"][0] for tile in ranked_grid_tiles if tile["core_box"][0] > 0})
|
||
internal_y = sorted({tile["core_box"][1] for tile in ranked_grid_tiles if tile["core_box"][1] > 0})
|
||
column_centers = sorted({min(ranked_width - 1, tile["core_box"][0] + tile["core_box"][2] // 2) for tile in ranked_grid_tiles[:3]})
|
||
row_centers = sorted({min(ranked_height - 1, ranked_grid_tiles[row * 3]["core_box"][1] + ranked_grid_tiles[row * 3]["core_box"][3] // 2) for row in range(3)})
|
||
ranked_seam_values = []
|
||
for seam_x in internal_x:
|
||
for center_y in row_centers:
|
||
ranked_seam_values.append(float(ranked_grid_weight_preview.array[0, center_y, seam_x, 0]))
|
||
for seam_y in internal_y:
|
||
for center_x in column_centers:
|
||
ranked_seam_values.append(float(ranked_grid_weight_preview.array[0, seam_y, center_x, 0]))
|
||
assert_equal(min(ranked_seam_values) > 0.45, True, "safe auto ranked 3x3 grid seams should keep both tile contributions instead of showing dark split lines")
|
||
|
||
prompt_builder = ttp.TTP_Smart_Tile_QwenVL_Prompt_Set_Builder_Experimental()
|
||
prompt_tile_set, prompt_set_json, prompt_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="template",
|
||
reference_image_mode="every_tile",
|
||
global_prompt="masterpiece",
|
||
global_negative="blurry",
|
||
)
|
||
prompt_data = json.loads(prompt_set_json)
|
||
assert_equal(prompt_data["type"], "ttp_smart_tile_prompt_set", "prompt builder should output prompt set JSON")
|
||
assert_equal(len(prompt_data["tiles"]), len(tile_set["tile_images"]), "prompt builder should create one prompt per tile")
|
||
assert_equal("masterpiece" in prompt_tile_set["tile_meta"]["tiles"][0]["prompt"], True, "prompt builder should merge global prompt")
|
||
assert_equal(prompt_tile_set["tile_meta"]["tiles"][0]["negative"], "blurry", "prompt builder should write tile negative prompts")
|
||
assert_equal("0:" in prompt_summary, True, "prompt builder should summarize prompts")
|
||
|
||
shifted_tile_set, shifted_prompt_json, _shifted_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="template",
|
||
reference_image_mode="every_tile",
|
||
system_prompt="tile_json_strict",
|
||
tile_instruction="custom system",
|
||
global_prompt="custom tile instruction",
|
||
global_negative="masterpiece",
|
||
prompt_merge_mode="blurry",
|
||
output_language="global_plus_caption",
|
||
max_new_tokens="chinese",
|
||
temperature=768,
|
||
prompt_preset=0.2,
|
||
)
|
||
shifted_prompt_data = json.loads(shifted_prompt_json)
|
||
assert_equal(shifted_prompt_data["prompt_preset"], "tile_json_strict", "prompt builder should recover preset from shifted widget values")
|
||
assert_equal(shifted_prompt_data["tiles"][0]["negative"], "blurry", "prompt builder should recover global negative from shifted widget values")
|
||
assert_equal("masterpiece" in shifted_tile_set["tile_meta"]["tiles"][0]["prompt"], True, "prompt builder should recover global prompt from shifted widget values")
|
||
|
||
class FakeQwenClip:
|
||
last_seed = None
|
||
last_text = ""
|
||
last_kwargs = {}
|
||
|
||
def tokenize(self, text, **kwargs):
|
||
self.last_text = text
|
||
self.last_kwargs = kwargs
|
||
return {"text": text, **kwargs}
|
||
|
||
def generate(self, tokens, **kwargs):
|
||
self.last_seed = kwargs.get("seed")
|
||
return ["fake_ids"]
|
||
|
||
def decode(self, output_ids, skip_special_tokens=True):
|
||
tile_index = "unknown"
|
||
marker = "Tile index:"
|
||
if marker in self.last_text:
|
||
tile_index = self.last_text.split(marker, 1)[1].split(".", 1)[0].strip()
|
||
return '{"label":"qwen tile %s","caption":"Qwen saw tile %s.","prompt":"qwen detailed tile %s prompt","negative":"qwen negative %s"}' % (tile_index, tile_index, tile_index, tile_index)
|
||
|
||
|
||
fake_qwen_clip = FakeQwenClip()
|
||
qwen_tile_set, qwen_prompt_json, qwen_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="template",
|
||
reference_image_mode="every_tile",
|
||
global_prompt="masterpiece",
|
||
global_negative="blurry",
|
||
qwen_seed=1234,
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_qwen_vl.safetensors",
|
||
"clip": fake_qwen_clip,
|
||
},
|
||
)
|
||
qwen_prompt_data = json.loads(qwen_prompt_json)
|
||
assert_equal(qwen_prompt_data["mode"], "qwen_vl_local", "connected local QwenVL model should override template fallback")
|
||
assert_equal(qwen_prompt_data["model_file"], "fake_qwen_vl.safetensors", "QwenVL prompt set JSON should record the connected local model file")
|
||
assert_equal(qwen_prompt_data["qwen_seed"], 1234, "QwenVL prompt set JSON should record the local sampling seed")
|
||
assert_equal(qwen_prompt_data["prompt_preset"], "tile_img2img_prompt", "QwenVL prompt set JSON should record the prompt preset")
|
||
assert_equal("mode=qwen_vl_local" in qwen_summary, True, "QwenVL summary should report the effective mode")
|
||
assert_equal(qwen_tile_set["tile_meta"]["tiles"][0]["prompt"], "qwen detailed tile 0 prompt", "QwenVL prompt should come from model output")
|
||
assert_equal(qwen_tile_set["tile_meta"]["tiles"][1]["prompt"], "qwen detailed tile 1 prompt", "QwenVL should write a distinct prompt per tile")
|
||
assert_equal(qwen_tile_set["tile_meta"]["tiles"][0]["qwen_cache"], "miss", "first QwenVL prompt build should miss tile cache")
|
||
assert_equal(fake_qwen_clip.last_seed, 1234, "local QwenVL generate should receive an integer seed")
|
||
assert_equal("llama_template" in fake_qwen_clip.last_kwargs, False, "local QwenVL should use ComfyUI native Qwen image chat template")
|
||
assert_equal("images" in fake_qwen_clip.last_kwargs, True, "local QwenVL should pass images through the native tokenizer path")
|
||
|
||
cached_qwen_tile_set, _cached_qwen_prompt_json, _cached_qwen_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="qwen_vl_local",
|
||
reference_image_mode="every_tile",
|
||
prompt_preset="tile_img2img_prompt",
|
||
global_prompt="masterpiece",
|
||
global_negative="blurry",
|
||
qwen_seed=1234,
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_qwen_vl.safetensors",
|
||
"clip": fake_qwen_clip,
|
||
},
|
||
)
|
||
assert_equal(cached_qwen_tile_set["tile_meta"]["tiles"][0]["qwen_cache"], "hit", "repeated QwenVL prompt build should hit tile cache")
|
||
|
||
legacy_seed_clip = FakeQwenClip()
|
||
_legacy_seed_tile_set, legacy_seed_prompt_json, _legacy_seed_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="qwen_vl_local",
|
||
seed=5678,
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_legacy_seed_qwen_vl.safetensors",
|
||
"clip": legacy_seed_clip,
|
||
},
|
||
)
|
||
legacy_seed_prompt_data = json.loads(legacy_seed_prompt_json)
|
||
assert_equal(legacy_seed_prompt_data["qwen_seed"], 5678, "legacy seed kwargs should map to qwen_seed")
|
||
assert_equal(legacy_seed_clip.last_seed, 5678, "legacy seed kwargs should still reach local QwenVL generate")
|
||
|
||
small_qwen_clip = FakeQwenClip()
|
||
small_qwen_tile_set, _small_qwen_prompt_json, _small_qwen_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="qwen_vl_local",
|
||
reference_image_mode="contact_sheet",
|
||
prompt_preset="tile_json_strict",
|
||
qwen_max_side=96,
|
||
qwen_max_pixels=0,
|
||
use_tile_cache=False,
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_small_qwen_vl.safetensors",
|
||
"clip": small_qwen_clip,
|
||
},
|
||
)
|
||
small_size = small_qwen_tile_set["tile_meta"]["tiles"][0]["qwen_input_size"]
|
||
assert_equal(max(small_size) <= 96, True, "QwenVL prompt builder should resize tile inputs for inference")
|
||
|
||
miswired_qwen_clip = FakeQwenClip()
|
||
miswired_tile_set, miswired_prompt_json, _miswired_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="template",
|
||
reference_image= {
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_miswired_qwen_vl.safetensors",
|
||
"clip": miswired_qwen_clip,
|
||
},
|
||
)
|
||
miswired_prompt_data = json.loads(miswired_prompt_json)
|
||
assert_equal(miswired_prompt_data["mode"], "qwen_vl_local", "miswired QwenVL model in reference_image should be recovered")
|
||
assert_equal(miswired_tile_set["tile_meta"]["tiles"][0]["prompt"], "qwen detailed tile 0 prompt", "recovered QwenVL model should still generate prompts")
|
||
|
||
class FakeQwenRetryClip(FakeQwenClip):
|
||
attempts = 0
|
||
|
||
def decode(self, output_ids, skip_special_tokens=True):
|
||
self.attempts += 1
|
||
if "Return compact JSON only" in self.last_text:
|
||
return '{"label":"retry tile","caption":"Recovered concise tile facts.","prompt":"recovered concise tile prompt","negative":"retry negative"}'
|
||
return "This response keeps describing the tile without JSON until it reaches the token limit."
|
||
|
||
|
||
retry_qwen_clip = FakeQwenRetryClip()
|
||
retry_tile_set, _retry_prompt_json, _retry_summary = prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="qwen_vl_local",
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_retry_qwen_vl.safetensors",
|
||
"clip": retry_qwen_clip,
|
||
},
|
||
)
|
||
assert_equal(retry_tile_set["tile_meta"]["tiles"][0]["prompt"], "recovered concise tile prompt", "QwenVL prompt builder should retry with compact JSON-only prompt")
|
||
assert_equal("qwen_raw_retry" in retry_tile_set["tile_meta"]["tiles"][0], True, "QwenVL retry raw output should be stored for debugging")
|
||
|
||
class FakeQwenEchoClip(FakeQwenClip):
|
||
def decode(self, output_ids, skip_special_tokens=True):
|
||
return "Refine this tile with attention to subject identity, clothing, pose, material, lighting, camera perspective, and local details. Preserve all visible visual facts without adding or altering non-visible elements."
|
||
|
||
|
||
try:
|
||
prompt_builder.build_prompt_set(
|
||
tile_set,
|
||
mode="qwen_vl_local",
|
||
qwen_vl_model={
|
||
"type": "ttp_qwenvl3_model",
|
||
"model_file": "fake_echo_qwen_vl.safetensors",
|
||
"clip": FakeQwenEchoClip(),
|
||
},
|
||
)
|
||
raise AssertionError("QwenVL instruction echo should not be accepted as a tile prompt")
|
||
except RuntimeError as exc:
|
||
assert_equal("did not return JSON fields" in str(exc), True, "QwenVL instruction echo should fail with a clear JSON error")
|
||
|
||
partial_qwen = '{ "label": "object_5", "caption": "close-up portrait with warm light", "prompt": "close-up portrait with warm'
|
||
partial_record = ttp._ttp_parse_qwen_tile_record(partial_qwen, 5)
|
||
assert_equal(partial_record["label"], "object_5", "partial Qwen JSON should preserve label")
|
||
assert_equal(partial_record["caption"], "close-up portrait with warm light", "partial Qwen JSON should preserve completed caption")
|
||
assert_equal(partial_record["prompt"], "close-up portrait with warm", "partial Qwen JSON should salvage truncated prompt text")
|
||
messy_qwen = 'assistant:\n<think>hidden reasoning</think>```json\n{"label":"face","caption":"sharp eyes","prompt":"detailed face","negative":""}\n```'
|
||
messy_record = ttp._ttp_parse_qwen_tile_record(messy_qwen, 0)
|
||
assert_equal(messy_record["label"], "face", "Qwen cleaner should remove assistant prefixes, markdown fences, and thinking blocks")
|
||
|
||
bbox_items = ttp._ttp_qwen_bbox_items('```json\n[{"label":"face","bbox":[300,200,500,450],"score":0.9}]\n```', 1000, 800)
|
||
assert_equal(len(bbox_items), 1, "Qwen bbox parser should read JSON list outputs")
|
||
assert_equal(bbox_items[0]["label"], "face", "Qwen bbox parser should preserve labels")
|
||
assert_equal(round(bbox_items[0]["x"]), 300, "Qwen bbox parser should convert normalized x coordinates")
|
||
single_bbox_item = ttp._ttp_qwen_bbox_items('{"label":"face","bbox_2d":[300,200,500,450],"score":0.9}', 1000, 800)
|
||
assert_equal(len(single_bbox_item), 1, "Qwen bbox parser should accept a single JSON object output")
|
||
wrapped_bbox_items = ttp._ttp_qwen_bbox_items('{"objects":[{"label":"hand","box_2d":[100,120,260,360],"score":0.8}]}', 1000, 800)
|
||
assert_equal(wrapped_bbox_items[0]["label"], "hand", "Qwen bbox parser should accept objects-wrapped outputs")
|
||
dict_bbox_items = ttp._ttp_qwen_bbox_items('{"label":"text","rect":{"x":100,"y":120,"width":200,"height":80},"score":0.7}', 1000, 800)
|
||
assert_equal(round(dict_bbox_items[0]["width"]), 200, "Qwen bbox parser should accept x/y/width/height rectangle outputs")
|
||
xywh_bbox_items = ttp._ttp_qwen_bbox_items('[{"label":"detail","bbox":[700,100,200,300],"bbox_format":"xywh"}]', 1000, 800)
|
||
assert_equal(round(xywh_bbox_items[0]["x"] + xywh_bbox_items[0]["width"]), 900, "Qwen bbox parser should accept xywh list outputs")
|
||
large_qwen_items, large_qwen_expanded = ttp._ttp_expand_single_large_qwen_bbox(
|
||
[{"x": 0, "y": 0, "width": 1000, "height": 800, "label": "full image", "score": 1.0}],
|
||
1000,
|
||
800,
|
||
8,
|
||
)
|
||
assert_equal(large_qwen_expanded, True, "Qwen auto tile should split a single full-frame bbox into useful regions")
|
||
assert_equal(len(large_qwen_items), 4, "Qwen large bbox fallback should create a 2x2 tile set")
|
||
|
||
loop_source = ttp.TTP_Smart_Tile_Loop_Source_Experimental()
|
||
loop_collect = ttp.TTP_Smart_Tile_Loop_Collect_Experimental()
|
||
|
||
class FakeClip:
|
||
def tokenize(self, text):
|
||
return {"text": text}
|
||
|
||
def encode_from_tokens_scheduled(self, tokens):
|
||
return [["conditioning", tokens["text"]]]
|
||
|
||
|
||
sam3_conditioning = ttp._ttp_encode_sam3_prompt_conditioning(FakeClip(), "person, face, hands", max_detections=2)
|
||
sam3_meta = sam3_conditioning[0][1]
|
||
assert_equal(len(sam3_meta["sam3_multi_cond"]), 3, "SAM3 internal prompt encoder should split comma-separated prompts")
|
||
assert_equal(sam3_meta["sam3_multi_cond"][0]["max_detections"], 2, "SAM3 internal prompt encoder should preserve max detections")
|
||
|
||
loop_image, loop_task, loop_index, loop_count, loop_done, loop_status, loop_prompt, loop_negative, loop_caption, loop_label, loop_prompt_tag, loop_positive, loop_negative_cond = loop_source.loop_source(
|
||
prompt_tile_set,
|
||
session_id="smoke_loop",
|
||
restart_request=1,
|
||
loop_request=1,
|
||
clip=FakeClip(),
|
||
unique_id="42",
|
||
)
|
||
assert_equal(loop_index, 0, "loop source should start at tile 0")
|
||
assert_equal(loop_count, len(tile_set["tile_images"]), "loop source should report tile count")
|
||
assert_equal(loop_done, False, "loop source should not be done at start")
|
||
assert_equal(loop_task["source_node_id"], "42", "loop task should carry source node id")
|
||
assert_equal(list(loop_image.shape[1:3]), [264, 368], "loop source should output a single real-size tile image")
|
||
assert_equal("masterpiece" in loop_prompt, True, "loop source should output the current tile prompt")
|
||
assert_equal(loop_negative, "blurry", "loop source should output the current tile negative prompt")
|
||
assert_equal(loop_positive, [["conditioning", loop_prompt]], "loop source should encode current tile positive prompt")
|
||
assert_equal(loop_negative_cond, [["conditioning", loop_negative]], "loop source should encode current tile negative prompt")
|
||
assert_equal(bool(loop_caption), True, "loop source should output the current tile caption")
|
||
processed_tile_set, done_after_first, next_index, collect_status = loop_collect.loop_collect(loop_task, loop_image)
|
||
assert_equal(done_after_first, False, "loop collect should continue after first tile")
|
||
assert_equal(next_index, 1, "loop collect should advance to tile 1")
|
||
assert_equal(processed_tile_set["type"], "ttp_smart_tile_set", "loop collect should output a processed tile set")
|
||
next_image, next_task, next_loop_index, _next_count, _next_done, _next_status, _next_prompt, _next_negative, _next_caption, _next_label, _next_prompt_tag, _next_positive, _next_negative_cond = loop_source.loop_source(
|
||
prompt_tile_set,
|
||
session_id="smoke_loop",
|
||
restart_request=1,
|
||
loop_request=2,
|
||
unique_id="42",
|
||
)
|
||
assert_equal(next_loop_index, 1, "loop source should emit the next tile after collect")
|
||
assert_equal(list(next_image.shape[1:3]), [264, 432], "loop source next tile should keep its own size")
|
||
assert_equal(_next_prompt != loop_prompt, True, "loop source should emit a distinct prompt for the next tile when prompts differ")
|
||
session = ttp._TTP_SMART_TILE_LOOP_SESSIONS["smoke_loop"]
|
||
session["index"] = len(tile_set["tile_images"]) - 1
|
||
last_image, last_task, _last_index, _last_count, _last_done, _last_status, _last_prompt, _last_negative, _last_caption, _last_label, _last_prompt_tag, _last_positive, _last_negative_cond = loop_source.loop_source(
|
||
prompt_tile_set,
|
||
session_id="smoke_loop",
|
||
restart_request=1,
|
||
loop_request=3,
|
||
unique_id="42",
|
||
)
|
||
_final_tile_set, final_done, final_next, final_status = loop_collect.loop_collect(last_task, last_image)
|
||
assert_equal(final_done, True, "loop collect should mark done after the last tile")
|
||
assert_equal("done" in final_status, True, "loop collect should report done status")
|
||
|
||
qwen_loop_image, qwen_loop_task, qwen_loop_index, _qwen_count, _qwen_done, _qwen_status, qwen_loop_prompt, _qwen_negative, _qwen_caption, _qwen_label, _qwen_prompt_tag, _qwen_positive, _qwen_negative_cond = loop_source.loop_source(
|
||
qwen_tile_set,
|
||
session_id="smoke_qwen_loop",
|
||
restart_request=1,
|
||
loop_request=1,
|
||
unique_id="43",
|
||
)
|
||
assert_equal(qwen_loop_prompt, "qwen detailed tile 0 prompt", "Qwen prompt tile 0 should reach loop source")
|
||
_qwen_processed, _qwen_done_first, _qwen_next_index, _qwen_collect_status = loop_collect.loop_collect(qwen_loop_task, qwen_loop_image)
|
||
_qwen_next_image, _qwen_next_task, qwen_next_index, _qwen_next_count, _qwen_next_done, _qwen_next_status, qwen_next_prompt, _qwen_next_negative, _qwen_next_caption, _qwen_next_label, _qwen_next_prompt_tag, _qwen_next_positive, _qwen_next_negative_cond = loop_source.loop_source(
|
||
qwen_tile_set,
|
||
session_id="smoke_qwen_loop",
|
||
restart_request=1,
|
||
loop_request=2,
|
||
unique_id="43",
|
||
)
|
||
assert_equal(qwen_next_index, 1, "Qwen loop should advance to tile 1")
|
||
assert_equal(qwen_next_prompt, "qwen detailed tile 1 prompt", "Qwen prompt tile 1 should reach loop source")
|
||
|
||
upscale_node = ttp.TTP_Smart_Tile_Image_Upscale_Prep_Experimental()
|
||
upscaled_tile, upscale_info = upscale_node.upscale_tile(loop_image, scale=1.5, round_to=16)
|
||
assert_equal(list(upscaled_tile.shape[1:3]), [400, 560], "upscale prep should scale and round tile dimensions")
|
||
assert_equal("368x264 -> 560x400" in upscale_info, True, "upscale prep should report size changes")
|
||
capped_tile, capped_info = upscale_node.upscale_tile(
|
||
ttp.pil2tensor(Image.new("RGB", (100, 100), "white")),
|
||
scale=4.0,
|
||
round_to=16,
|
||
max_megapixels=0.04,
|
||
use_upscale_model=False,
|
||
)
|
||
assert_equal(list(capped_tile.shape[1:3]), [192, 192], "upscale prep should round down under the megapixel cap")
|
||
assert_equal(capped_tile.shape[1] * capped_tile.shape[2] <= 40000, True, "upscale prep should stay under max megapixels after rounding")
|
||
assert_equal("max_megapixels=0.04 capped" in capped_info, True, "upscale prep should report megapixel capping")
|
||
semantic_node = ttp.TTP_Smart_Tile_Semantic_Rank_Experimental()
|
||
semantic_input_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [100, 100],
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [100, 100],
|
||
"tiles": [
|
||
{"name": "bg", "label": "background", "sample_box": [0, 0, 100, 100]},
|
||
{"name": "face", "label": "face", "caption": "portrait face", "sample_box": [20, 20, 50, 50]},
|
||
{"name": "eyes", "label": "eyes", "caption": "sharp eyes and eyelashes", "sample_box": [35, 35, 20, 12]},
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (100, 100), "white")),
|
||
ttp.pil2tensor(Image.new("RGB", (50, 50), "white")),
|
||
ttp.pil2tensor(Image.new("RGB", (80, 48), "white")),
|
||
],
|
||
}
|
||
ranked_tile_set, semantic_report = semantic_node.rank_tiles(semantic_input_tile_set)
|
||
ranked_tiles = ranked_tile_set["tile_meta"]["tiles"]
|
||
assert_equal(ranked_tiles[0]["semantic_category"], "background", "semantic rank should detect background tiles")
|
||
assert_equal(ranked_tiles[1]["semantic_category"], "face", "semantic rank should detect face tiles")
|
||
assert_equal(ranked_tiles[2]["semantic_category"], "eyes", "semantic rank should detect eye detail tiles")
|
||
assert_equal(ranked_tiles[2]["semantic_score"] > ranked_tiles[1]["semantic_score"] > ranked_tiles[0]["semantic_score"], True, "semantic rank should score detail tiles above face and background")
|
||
assert_equal(ranked_tiles[2]["recommended_composite_mode"], "soft_overlay", "semantic rank should recommend soft overlay for eye detail tiles")
|
||
assert_equal(ranked_tiles[2]["occlusion_priority"] > ranked_tiles[1]["occlusion_priority"] > ranked_tiles[0]["occlusion_priority"], True, "semantic rank should write composite priority metadata")
|
||
assert_equal("category=eyes" in semantic_report, True, "semantic rank report should include ranked categories")
|
||
size_node = ttp.TTP_Smart_Tile_Output_Size_Estimate_Experimental()
|
||
mixed_scale_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [100, 50],
|
||
"tiles": [
|
||
{
|
||
"name": "left",
|
||
"sample_box": [0, 0, 50, 50],
|
||
"tile_canvas_size": [50, 50],
|
||
"tile_canvas_box": [0, 0, 50, 50],
|
||
},
|
||
{
|
||
"name": "right",
|
||
"sample_box": [50, 0, 50, 50],
|
||
"tile_canvas_size": [50, 50],
|
||
"tile_canvas_box": [0, 0, 50, 50],
|
||
},
|
||
],
|
||
}
|
||
mixed_scale_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": mixed_scale_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (200, 200), "white")),
|
||
ttp.pil2tensor(Image.new("RGB", (100, 100), "white")),
|
||
],
|
||
}
|
||
deferred_scale, deferred_w, deferred_h, deferred_x, deferred_y, deferred_info = size_node.estimate_output_size(mixed_scale_tile_set, done=False)
|
||
assert_equal([deferred_scale, deferred_w, deferred_h, deferred_x, deferred_y], [1.0, 100, 50, 1.0, 1.0], "output size estimate should return safe pending dimensions before loop completion")
|
||
assert_equal("deferred" in deferred_info, True, "output size estimate should report deferred status")
|
||
size_scale, size_w, size_h, size_scale_x, size_scale_y, size_info = size_node.estimate_output_size(mixed_scale_tile_set)
|
||
assert_equal(round(size_scale, 4), 3.0, "output size estimate should match assemble median scale inference")
|
||
assert_equal([size_w, size_h], [300, 150], "output size estimate should report final resolution")
|
||
assert_equal(round(size_scale_x, 4), 3.0, "output size estimate should report median x scale")
|
||
assert_equal(round(size_scale_y, 4), 3.0, "output size estimate should report median y scale")
|
||
assert_equal("warning=mixed_tile_scales" in size_info, True, "output size estimate should warn about mixed tile scales")
|
||
min_scale, min_w, min_h, _min_x, _min_y, _min_info = size_node.estimate_output_size(mixed_scale_tile_set, scale_strategy="min")
|
||
assert_equal(round(min_scale, 4), 2.0, "output size estimate should support min strategy")
|
||
assert_equal([min_w, min_h], [200, 100], "output size estimate min strategy should resize final resolution")
|
||
focus_scale_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [100, 50],
|
||
"tiles": [
|
||
{
|
||
"label": "background",
|
||
"sample_box": [0, 0, 100, 50],
|
||
"tile_canvas_size": [100, 50],
|
||
"tile_canvas_box": [0, 0, 100, 50],
|
||
},
|
||
{
|
||
"label": "eyes",
|
||
"sample_box": [25, 10, 10, 10],
|
||
"tile_canvas_size": [10, 10],
|
||
"tile_canvas_box": [0, 0, 10, 10],
|
||
},
|
||
],
|
||
}
|
||
focus_scale_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": focus_scale_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (100, 50), "white")),
|
||
ttp.pil2tensor(Image.new("RGB", (40, 40), "white")),
|
||
],
|
||
}
|
||
focus_scale, focus_w, focus_h, focus_x, focus_y, focus_info = size_node.estimate_output_size(focus_scale_tile_set, scale_strategy="focus_weighted")
|
||
assert_equal(round(focus_scale, 4), 4.0, "focus-weighted output size should prefer high-detail tile scale")
|
||
assert_equal([focus_w, focus_h], [400, 200], "focus-weighted output size should report focus-detail resolution")
|
||
assert_equal(round(focus_x, 4), 4.0, "focus-weighted x scale should prefer high-detail tile scale")
|
||
assert_equal(round(focus_y, 4), 4.0, "focus-weighted y scale should prefer high-detail tile scale")
|
||
assert_equal("weight=" in focus_info, True, "focus-weighted info should report semantic weights")
|
||
done_scale, done_w, done_h, _done_x, _done_y, _done_info = size_node.estimate_output_size(mixed_scale_tile_set, done=True)
|
||
assert_equal(round(done_scale, 4), 3.0, "output size estimate should compute when done is true")
|
||
assert_equal([done_w, done_h], [300, 150], "output size estimate done gate should preserve final resolution")
|
||
aligned = ttp._ttp_align_pil_to_aspect(Image.new("RGB", (561, 401), "white"), 560, 400, "center_crop")
|
||
assert_equal(aligned.size, (560, 400), "assemble alignment should crop/resize drifted sampler output")
|
||
resized = ttp._ttp_align_pil_to_aspect(Image.new("RGB", (500, 500), "white"), 560, 400, "resize")
|
||
assert_equal(resized.size, (560, 400), "assemble resize alignment should force expected size")
|
||
save_final_node = ttp.TTP_Smart_Tile_Save_Final_Image_Experimental()
|
||
skip_result = save_final_node.save_final_image(upscaled_tile, done=False)
|
||
assert_equal(skip_result, {"ui": {"images": []}}, "save final image should skip intermediate loop frames")
|
||
|
||
assert_equal(
|
||
tiles_meta[0]["overlap_edges_px_source"],
|
||
{"left": 0, "right": 64, "top": 0, "bottom": 64},
|
||
"top-left grid overlap edges",
|
||
)
|
||
assert_equal(
|
||
tiles_meta[4]["overlap_edges_px_source"],
|
||
{"left": 64, "right": 64, "top": 64, "bottom": 64},
|
||
"center grid overlap edges",
|
||
)
|
||
assert_equal(
|
||
tiles_meta[8]["overlap_edges_px_source"],
|
||
{"left": 64, "right": 0, "top": 64, "bottom": 0},
|
||
"bottom-right grid overlap edges",
|
||
)
|
||
|
||
gappy_layout = json.dumps({
|
||
"tiles": [
|
||
{"name": "left", "x0": 0, "y0": 0, "x1": 0.25, "y1": 1},
|
||
{"name": "right", "x0": 0.5, "y0": 0, "x1": 1, "y1": 1},
|
||
]
|
||
}, separators=(",", ":"))
|
||
gappy_normalized = ttp._ttp_interactive_layout_with_defaults(gappy_layout, 64, 32, False)
|
||
gappy_meta = ttp._ttp_parse_smart_tile_layout(gappy_normalized, 800, 600)
|
||
assert_equal(
|
||
[tile["sample_box"] for tile in gappy_meta],
|
||
[[0, 0, 200, 600], [400, 0, 400, 600]],
|
||
"non-adjacent manual tiles must not overlap across a gap",
|
||
)
|
||
assert_equal(
|
||
[tile["overlap_edges_px_source"] for tile in gappy_meta],
|
||
[
|
||
{"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
{"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
],
|
||
"non-adjacent manual overlap edges",
|
||
)
|
||
|
||
touching_layout = json.dumps({
|
||
"tiles": [
|
||
{"name": "left", "x0": 0, "y0": 0, "x1": 0.5, "y1": 1},
|
||
{"name": "right", "x0": 0.5, "y0": 0, "x1": 1, "y1": 1},
|
||
]
|
||
}, separators=(",", ":"))
|
||
touching_normalized = ttp._ttp_interactive_layout_with_defaults(touching_layout, 64, 32, False)
|
||
touching_meta = ttp._ttp_parse_smart_tile_layout(touching_normalized, 800, 600)
|
||
assert_equal(
|
||
[tile["sample_box"] for tile in touching_meta],
|
||
[[0, 0, 464, 600], [336, 0, 464, 600]],
|
||
"adjacent manual tiles should overlap only on the shared edge",
|
||
)
|
||
assert_equal(
|
||
[tile["overlap_edges_px_source"] for tile in touching_meta],
|
||
[
|
||
{"left": 0, "right": 64, "top": 0, "bottom": 0},
|
||
{"left": 64, "right": 0, "top": 0, "bottom": 0},
|
||
],
|
||
"adjacent manual overlap edges",
|
||
)
|
||
|
||
right_bottom_mask = ttp._ttp_create_sample_blend_mask(
|
||
432,
|
||
328,
|
||
{"left": 0, "right": 132, "top": 0, "bottom": 128},
|
||
32,
|
||
)
|
||
assert_equal(float(right_bottom_mask[100, 0, 0]), 1.0, "non-overlap left edge should stay fully weighted")
|
||
assert_equal(float(right_bottom_mask[0, 100, 0]), 1.0, "non-overlap top edge should stay fully weighted")
|
||
assert_equal(float(right_bottom_mask[100, 300, 0]), 1.0, "right overlap should start at full weight")
|
||
assert_equal(float(right_bottom_mask[100, 431, 0]), 0.0, "right overlap outer edge should fade out")
|
||
assert_equal(float(right_bottom_mask[200, 100, 0]), 1.0, "bottom overlap should start at full weight")
|
||
assert_equal(float(right_bottom_mask[327, 100, 0]), 0.0, "bottom overlap outer edge should fade out")
|
||
|
||
left_top_mask = ttp._ttp_create_sample_blend_mask(
|
||
432,
|
||
328,
|
||
{"left": 64, "right": 0, "top": 64, "bottom": 0},
|
||
32,
|
||
)
|
||
assert_equal(float(left_top_mask[100, 0, 0]), 0.0, "left overlap outer edge should fade in")
|
||
assert_equal(float(left_top_mask[100, 63, 0]), 1.0, "left overlap should reach full weight at the core")
|
||
assert_equal(float(left_top_mask[0, 100, 0]), 0.0, "top overlap outer edge should fade in")
|
||
assert_equal(float(left_top_mask[63, 100, 0]), 1.0, "top overlap should reach full weight at the core")
|
||
assert_equal(float(left_top_mask[100, 431, 0]), 1.0, "non-overlap right edge should stay fully weighted")
|
||
assert_equal(float(left_top_mask[327, 100, 0]), 1.0, "non-overlap bottom edge should stay fully weighted")
|
||
|
||
auto_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[{"x": 250, "y": 120, "width": 180, "height": 260, "label": "person", "score": 0.91}],
|
||
900,
|
||
600,
|
||
default_pad=64,
|
||
default_blend=32,
|
||
object_padding=40,
|
||
max_tiles=4,
|
||
include_background=True,
|
||
allow_object_overlap=True,
|
||
)
|
||
auto_meta = ttp._ttp_parse_smart_tile_layout(auto_layout, 900, 600)
|
||
assert_equal(len(auto_meta), 2, "auto layout should include background and one object tile")
|
||
object_tile = auto_meta[1]
|
||
ox, oy, ow, oh = object_tile["core_box"]
|
||
assert_equal(ox <= 250 and oy <= 120 and ox + ow >= 430 and oy + oh >= 380, True, "auto object tile must contain the source bbox")
|
||
assert_equal(object_tile["source"], "auto", "auto layout should preserve source metadata")
|
||
assert_equal(object_tile["label"], "person", "auto layout should preserve labels")
|
||
assert_equal(object_tile["layer"], 2, "auto object tile should be above background")
|
||
assert_equal(object_tile["occlusion_priority"] > auto_meta[0]["occlusion_priority"], True, "object tile should outrank background")
|
||
|
||
detail_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[
|
||
{"x": 220, "y": 80, "width": 300, "height": 440, "label": "person", "score": 0.95},
|
||
{"x": 315, "y": 120, "width": 92, "height": 86, "label": "face", "score": 0.88},
|
||
],
|
||
900,
|
||
600,
|
||
default_pad=64,
|
||
default_blend=32,
|
||
object_padding=24,
|
||
max_tiles=4,
|
||
include_background=True,
|
||
allow_object_overlap=True,
|
||
)
|
||
detail_meta = ttp._ttp_parse_smart_tile_layout(detail_layout, 900, 600)
|
||
person_tile = next(tile for tile in detail_meta if tile.get("label") == "person")
|
||
face_tile = next(tile for tile in detail_meta if tile.get("label") == "face")
|
||
assert_equal(face_tile["layer"] > person_tile["layer"], True, "face/detail auto tile should sit above generic object tile")
|
||
assert_equal(face_tile["occlusion_priority"] > person_tile["occlusion_priority"], True, "face/detail auto tile should outrank generic object tile")
|
||
|
||
mask_image = Image.new("L", (900, 600), 0)
|
||
for x in range(260, 420):
|
||
for y in range(140, 360):
|
||
mask_image.putpixel((x, y), 255)
|
||
masked_auto_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[{"x": 250, "y": 120, "width": 180, "height": 260, "label": "person", "score": 0.91}],
|
||
900,
|
||
600,
|
||
default_pad=64,
|
||
default_blend=32,
|
||
object_padding=40,
|
||
max_tiles=4,
|
||
include_background=False,
|
||
allow_object_overlap=True,
|
||
masks=[mask_image],
|
||
)
|
||
masked_auto_meta = ttp._ttp_parse_smart_tile_layout(masked_auto_layout, 900, 600)
|
||
assert_equal("object_mask" in masked_auto_meta[0], True, "auto layout should preserve SAM object masks")
|
||
mask_array = ttp._ttp_tile_object_mask_array(masked_auto_meta[0], masked_auto_meta[0]["sample_box"][2], masked_auto_meta[0]["sample_box"][3], "mask_feather", 32)
|
||
assert_equal(mask_array.shape[2], 1, "decoded object mask should be a single-channel weight map")
|
||
assert_equal(float(mask_array.max()) > 0.5, True, "decoded object mask should keep foreground weight")
|
||
|
||
small_expand_mask = Image.new("L", (32, 32), 0)
|
||
for x in range(10, 14):
|
||
for y in range(10, 14):
|
||
small_expand_mask.putpixel((x, y), 255)
|
||
small_mask_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[{"x": 10, "y": 10, "width": 4, "height": 4, "label": "eye", "score": 0.9}],
|
||
32,
|
||
32,
|
||
object_padding=4,
|
||
mask_expand=0,
|
||
max_tiles=1,
|
||
include_background=False,
|
||
masks=[small_expand_mask],
|
||
)
|
||
expanded_mask_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[{"x": 10, "y": 10, "width": 4, "height": 4, "label": "eye", "score": 0.9}],
|
||
32,
|
||
32,
|
||
object_padding=4,
|
||
mask_expand=2,
|
||
max_tiles=1,
|
||
include_background=False,
|
||
masks=[small_expand_mask],
|
||
)
|
||
small_mask_tile = ttp._ttp_parse_smart_tile_layout(small_mask_layout, 32, 32)[0]
|
||
expanded_mask_tile = ttp._ttp_parse_smart_tile_layout(expanded_mask_layout, 32, 32)[0]
|
||
small_mask_pixels = np.count_nonzero(np.array(ttp._ttp_decode_object_mask_data(small_mask_tile["object_mask"])) > 0)
|
||
expanded_mask_pixels = np.count_nonzero(np.array(ttp._ttp_decode_object_mask_data(expanded_mask_tile["object_mask"])) > 0)
|
||
assert_equal(expanded_mask_pixels > small_mask_pixels, True, "auto mask expand should dilate object mask foreground pixels")
|
||
assert_equal(expanded_mask_tile.get("object_mask_expand"), 2.0, "auto mask expand should be preserved in tile metadata")
|
||
|
||
large_body_mask = Image.new("L", (900, 600), 255)
|
||
large_body_layout = ttp._ttp_boxes_to_auto_layout(
|
||
[{"x": 0, "y": 0, "width": 900, "height": 600, "label": "person body", "score": 0.93}],
|
||
900,
|
||
600,
|
||
default_pad=64,
|
||
default_blend=32,
|
||
object_padding=0,
|
||
max_tiles=4,
|
||
include_background=False,
|
||
allow_object_overlap=True,
|
||
masks=[large_body_mask],
|
||
)
|
||
large_body_meta = ttp._ttp_parse_smart_tile_layout(large_body_layout, 900, 600)
|
||
large_body_tile = large_body_meta[0]
|
||
assert_equal("large context" in large_body_tile["label"], True, "huge non-detail auto body tiles should be marked as large context")
|
||
assert_equal(large_body_tile["layer"], 0, "huge non-detail auto body tiles should stay on the lowest layer")
|
||
assert_equal(large_body_tile["occlusion_priority"], 0, "huge non-detail auto body tiles should not outrank face/detail masks")
|
||
assert_equal(float(large_body_tile["priority"]) <= 10.0, True, "huge non-detail auto body tiles should keep low composite priority")
|
||
assert_equal(float(large_body_tile["importance"]) <= 0.35, True, "huge non-detail auto body tiles should remain a low-weight context layer")
|
||
assert_equal("object_mask" in large_body_tile, True, "huge body context tiles should still preserve their body mask")
|
||
assert_equal(large_body_tile["recommended_composite_mode"], "context", "huge body context tiles should advertise context compositing")
|
||
|
||
semantic_mask_layout = json.dumps({
|
||
"tiles": [{
|
||
"name": "person_grid_1",
|
||
"x0": 0.2,
|
||
"y0": 0.2,
|
||
"x1": 0.5,
|
||
"y1": 0.6,
|
||
"label": "person",
|
||
"semantic_category": "subject",
|
||
"recommended_scale_weight": 1.75,
|
||
"recommended_composite_mode": "replace",
|
||
"recommended_occlusion_priority": 6200,
|
||
"object_mask": masked_auto_meta[0]["object_mask"],
|
||
"object_mask_source": masked_auto_meta[0]["object_mask"],
|
||
}],
|
||
}, separators=(",", ":"))
|
||
semantic_mask_meta = ttp._ttp_parse_smart_tile_layout(semantic_mask_layout, 900, 600)
|
||
assert_equal(semantic_mask_meta[0]["semantic_category"], "subject", "interactive parser should preserve inherited semantic category")
|
||
assert_equal(round(semantic_mask_meta[0]["recommended_scale_weight"], 2), 1.75, "interactive parser should preserve inherited scale weight")
|
||
assert_equal(semantic_mask_meta[0]["recommended_composite_mode"], "replace", "interactive parser should preserve inherited composite mode")
|
||
assert_equal("object_mask" in semantic_mask_meta[0], True, "interactive parser should preserve inherited cropped object mask")
|
||
assert_equal("object_mask_source" in semantic_mask_meta[0], True, "interactive parser should preserve parent object mask source for refresh")
|
||
|
||
paint_mask = Image.new("L", (64, 48), 0)
|
||
for x in range(8, 22):
|
||
for y in range(6, 20):
|
||
paint_mask.putpixel((x, y), 255)
|
||
for x in range(42, 56):
|
||
for y in range(28, 42):
|
||
paint_mask.putpixel((x, y), 255)
|
||
paint_buffer = BytesIO()
|
||
paint_mask.save(paint_buffer, format="PNG")
|
||
paint_payload = json.dumps({
|
||
"format": "png_base64",
|
||
"width": 64,
|
||
"height": 48,
|
||
"data": base64.b64encode(paint_buffer.getvalue()).decode("ascii"),
|
||
})
|
||
decoded_paint = ttp._ttp_decode_interactive_paint_mask(paint_payload, 64, 48)
|
||
paint_items, paint_masks = ttp._ttp_paint_mask_to_items(decoded_paint, 64, 48)
|
||
assert_equal(len(paint_items), 2, "paint mask should become one bbox per painted island")
|
||
paint_layout, paint_message = ttp._ttp_run_paint_mask_auto_layout(
|
||
Image.new("RGB", (64, 48), "black"),
|
||
paint_payload,
|
||
default_pad=8,
|
||
default_blend=4,
|
||
object_padding=2,
|
||
mask_expand=2,
|
||
max_tiles=8,
|
||
allow_object_overlap=True,
|
||
)
|
||
paint_meta = ttp._ttp_parse_smart_tile_layout(paint_layout, 64, 48)
|
||
assert_equal(len(paint_meta), 2, "paint-only auto tile should create tiles from painted regions")
|
||
assert_equal(all(tile.get("object_mask") for tile in paint_meta), True, "paint-created tiles should carry object masks")
|
||
assert_equal("paint mask" in paint_message.lower(), True, "paint-only auto tile should report paint mask inference")
|
||
|
||
half_mask = Image.new("L", (8, 8), 128)
|
||
half_mask_data = ttp._ttp_encode_object_mask_data([half_mask], [0], [0, 0, 8, 8])
|
||
assemble_node = ttp.TTP_Smart_Tile_Assemble_Experimental()
|
||
stack_assemble_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [8, 8],
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [8, 8],
|
||
"tiles": [
|
||
{
|
||
"name": f"stack_tile_{index + 1}",
|
||
"core_box": [0, 0, 8, 8],
|
||
"sample_box": [0, 0, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 1 if index == 3 else 0,
|
||
"occlusion_priority": 80000 if index == 3 else 0,
|
||
}
|
||
for index in range(4)
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (220, 20, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 220, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (220, 220, 20)))[0],
|
||
],
|
||
}
|
||
stack_assemble_set, _stack_assemble_json, _stack_assemble_summary = stack_node.apply_stack_order(
|
||
stack_assemble_tile_set,
|
||
tile_order="T1,T3,T2",
|
||
)
|
||
stack_assembled, _stack_assembled_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=stack_assemble_set,
|
||
)
|
||
stack_center = stack_assembled.array[0, 4, 4]
|
||
assert_equal(float(stack_center[0]) > float(stack_center[2]), True, "stack order assemble should put the first listed tile on top by default")
|
||
assert_equal(float(stack_center[0]) > float(stack_center[1]), True, "stack order assemble should keep unlisted and later listed tiles behind the first listed tile")
|
||
soft_tile_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [8, 8],
|
||
"tiles": [{
|
||
"name": "soft_object",
|
||
"core_box": [0, 0, 8, 8],
|
||
"sample_box": [0, 0, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 2,
|
||
"occlusion_priority": 10,
|
||
"object_mask": half_mask_data,
|
||
}],
|
||
}
|
||
soft_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": soft_tile_meta,
|
||
"tile_images": [ttp.pil2tensor(Image.new("RGB", (8, 8), "white"))[0]],
|
||
}
|
||
soft_output, _soft_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
mask_blend_mode="mask_only",
|
||
tile_set=soft_tile_set,
|
||
base_image=ttp.pil2tensor(Image.new("RGB", (8, 8), "black")),
|
||
)
|
||
soft_value = float(soft_output.array[0, 4, 4, 0])
|
||
assert_equal(0.45 < soft_value < 0.55, True, "occlusion mask feather should alpha blend with lower pixels instead of hard replacing them")
|
||
|
||
blue_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": soft_tile_meta,
|
||
"tile_images": [ttp.pil2tensor(Image.new("RGB", (8, 8), (32, 64, 192)))[0]],
|
||
}
|
||
red_reference = ttp.pil2tensor(Image.new("RGB", (8, 8), (192, 64, 32)))
|
||
color_matched, _color_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
color_correction="local_mean_std",
|
||
color_strength=1.0,
|
||
mask_blend_mode="mask_only",
|
||
tile_set=blue_tile_set,
|
||
base_image=red_reference,
|
||
color_reference_image=red_reference,
|
||
)
|
||
matched_pixel = color_matched.array[0, 4, 4]
|
||
assert_equal(float(matched_pixel[0]) > float(matched_pixel[2]), True, "local mean/std color correction should match tile color toward the local reference")
|
||
|
||
black_canvas, _black_canvas_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
mask_blend_mode="mask_only",
|
||
base_canvas_mode="black",
|
||
tile_set=blue_tile_set,
|
||
base_image=red_reference,
|
||
)
|
||
black_canvas_pixel = black_canvas.array[0, 4, 4]
|
||
assert_equal(float(black_canvas_pixel[2]) > float(black_canvas_pixel[0]), True, "black canvas mode should not mix the connected base image into semi-transparent tiles")
|
||
|
||
black_canvas_color_matched, _black_canvas_color_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
color_correction="local_mean_std",
|
||
color_strength=1.0,
|
||
mask_blend_mode="mask_only",
|
||
base_canvas_mode="black",
|
||
tile_set=blue_tile_set,
|
||
base_image=red_reference,
|
||
)
|
||
black_canvas_color_pixel = black_canvas_color_matched.array[0, 4, 4]
|
||
assert_equal(float(black_canvas_color_pixel[0]) > float(black_canvas_color_pixel[2]), True, "black canvas mode should still allow base_image color reference")
|
||
|
||
stack_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [8, 8],
|
||
"tiles": [
|
||
{
|
||
"name": "large_body",
|
||
"label": "person body",
|
||
"core_box": [0, 0, 8, 8],
|
||
"sample_box": [0, 0, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 100.0,
|
||
"layer": 2,
|
||
"occlusion_priority": 1000,
|
||
},
|
||
{
|
||
"name": "small_face",
|
||
"label": "local detail",
|
||
"core_box": [2, 2, 4, 4],
|
||
"sample_box": [2, 2, 4, 4],
|
||
"tile_canvas_size": [4, 4],
|
||
"tile_canvas_box": [0, 0, 4, 4],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
},
|
||
],
|
||
}
|
||
stack_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": stack_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (220, 20, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (4, 4), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
large_above, _large_above_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
auto_composite_policy="strict_layer",
|
||
tile_set=stack_tile_set,
|
||
)
|
||
large_above_pixel = large_above.array[0, 4, 4]
|
||
assert_equal(float(large_above_pixel[0]) > float(large_above_pixel[2]), True, "explicit large tile priority should cover a small tile when small_tile_on_top is disabled")
|
||
small_above, _small_above_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
small_tile_on_top=True,
|
||
auto_composite_policy="strict_layer",
|
||
tile_set=stack_tile_set,
|
||
)
|
||
small_above_pixel = small_above.array[0, 4, 4]
|
||
assert_equal(float(small_above_pixel[2]) > float(small_above_pixel[0]), True, "small_tile_on_top should stack smaller tiles above larger context tiles")
|
||
|
||
legacy_bad_bool, _legacy_bad_bool_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
small_tile_on_top="safe_auto",
|
||
auto_composite_policy="strict_layer",
|
||
tile_set=stack_tile_set,
|
||
)
|
||
legacy_bad_bool_pixel = legacy_bad_bool.array[0, 4, 4]
|
||
assert_equal(float(legacy_bad_bool_pixel[0]) > float(legacy_bad_bool_pixel[2]), True, "legacy string values should not be treated as small_tile_on_top=true")
|
||
|
||
fingerprint_mask_a = Image.new("L", (8, 8), 0)
|
||
ImageDraw.Draw(fingerprint_mask_a).rectangle((0, 0, 3, 7), fill=255)
|
||
fingerprint_mask_b = Image.new("L", (8, 8), 0)
|
||
ImageDraw.Draw(fingerprint_mask_b).rectangle((4, 0, 7, 7), fill=255)
|
||
fingerprint_source_a = ttp._ttp_encode_object_mask_data([fingerprint_mask_a], [0], [0, 0, 8, 8])
|
||
fingerprint_source_b = ttp._ttp_encode_object_mask_data([fingerprint_mask_b], [0], [0, 0, 8, 8])
|
||
fingerprint_tile = {
|
||
"name": "context_grid_1",
|
||
"label": "large context",
|
||
"core_box": [0, 0, 8, 8],
|
||
"sample_box": [0, 0, 8, 8],
|
||
"object_mask": fingerprint_source_a,
|
||
"object_mask_source": fingerprint_source_a,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
}
|
||
fingerprint_tile_set_a = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [8, 8],
|
||
"tile_meta": {"type": "ttp_smart_tile", "original_size": [8, 8], "tiles": [fingerprint_tile]},
|
||
"tile_images": [None],
|
||
}
|
||
fingerprint_tile_b = dict(fingerprint_tile)
|
||
fingerprint_tile_b["object_mask_source"] = fingerprint_source_b
|
||
fingerprint_tile_set_b = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [8, 8],
|
||
"tile_meta": {"type": "ttp_smart_tile", "original_size": [8, 8], "tiles": [fingerprint_tile_b]},
|
||
"tile_images": [None],
|
||
}
|
||
assert_equal(
|
||
ttp._ttp_tile_set_fingerprint(fingerprint_tile_set_a) != ttp._ttp_tile_set_fingerprint(fingerprint_tile_set_b),
|
||
True,
|
||
"loop fingerprint should change when object_mask_source changes",
|
||
)
|
||
|
||
feather_stack_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [16, 16],
|
||
"tiles": [
|
||
{
|
||
"name": "background",
|
||
"label": "background",
|
||
"core_box": [0, 0, 16, 16],
|
||
"sample_box": [0, 0, 16, 16],
|
||
"tile_canvas_size": [16, 16],
|
||
"tile_canvas_box": [0, 0, 16, 16],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
},
|
||
{
|
||
"name": "face_detail",
|
||
"label": "face detail",
|
||
"core_box": [4, 4, 8, 8],
|
||
"sample_box": [4, 4, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 4,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 2,
|
||
"occlusion_priority": 1000,
|
||
},
|
||
],
|
||
}
|
||
feather_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": feather_stack_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (16, 16), (220, 20, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
feathered_stack, _feathered_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
tile_set=feather_tile_set,
|
||
)
|
||
feather_edge_pixel = feathered_stack.array[0, 4, 8]
|
||
feather_center_pixel = feathered_stack.array[0, 8, 8]
|
||
assert_equal(float(feather_edge_pixel[0]) > float(feather_edge_pixel[2]), True, "focus tile fallback feather should keep lower pixels visible at the rectangle edge")
|
||
assert_equal(float(feather_center_pixel[2]) > float(feather_center_pixel[0]), True, "focus tile fallback feather should keep the focus tile strong in the center")
|
||
|
||
eye_mask_full = Image.new("L", (16, 16), 0)
|
||
eye_mask_tile = Image.new("L", (8, 8), 128)
|
||
ImageDraw.Draw(eye_mask_tile).rectangle((2, 2, 5, 5), fill=255)
|
||
eye_mask_full.paste(eye_mask_tile, (4, 4))
|
||
eye_mask_data = ttp._ttp_encode_object_mask_data([eye_mask_full], [0], [4, 4, 12, 12])
|
||
eye_stack_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [16, 16],
|
||
"tiles": [
|
||
{
|
||
"name": "face_base",
|
||
"label": "face",
|
||
"core_box": [0, 0, 16, 16],
|
||
"sample_box": [0, 0, 16, 16],
|
||
"tile_canvas_size": [16, 16],
|
||
"tile_canvas_box": [0, 0, 16, 16],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 1,
|
||
"occlusion_priority": 100,
|
||
},
|
||
{
|
||
"name": "eyes_detail",
|
||
"label": "eyes glasses",
|
||
"core_box": [4, 4, 8, 8],
|
||
"sample_box": [4, 4, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 4,
|
||
"occlusion_priority": 2000,
|
||
"object_mask": eye_mask_data,
|
||
},
|
||
],
|
||
}
|
||
eye_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": eye_stack_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (16, 16), (220, 20, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
strict_eye, _strict_eye_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="strict_layer",
|
||
tile_set=eye_tile_set,
|
||
)
|
||
safe_eye, _safe_eye_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=eye_tile_set,
|
||
)
|
||
strict_eye_edge = strict_eye.array[0, 4, 8]
|
||
safe_eye_edge = safe_eye.array[0, 4, 8]
|
||
safe_eye_center = safe_eye.array[0, 8, 8]
|
||
assert_equal(float(safe_eye_edge[0]) > float(strict_eye_edge[0]), True, "safe auto detail overlay should preserve more face color at eye mask edges")
|
||
assert_equal(float(safe_eye_center[2]) > float(safe_eye_center[0]), True, "safe auto detail overlay should keep eye details stronger at the mask center")
|
||
|
||
replace_mask_full = Image.new("L", (16, 16), 0)
|
||
ImageDraw.Draw(replace_mask_full).rectangle((7, 7, 8, 8), fill=255)
|
||
replace_mask_data = ttp._ttp_encode_object_mask_data([replace_mask_full], [0], [4, 4, 12, 12])
|
||
replace_mask_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [16, 16],
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [16, 16],
|
||
"tiles": [
|
||
{
|
||
"name": "masked_edit",
|
||
"label": "paint mask edit",
|
||
"core_box": [4, 4, 8, 8],
|
||
"sample_box": [4, 4, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
"object_mask": replace_mask_data,
|
||
"composite_mode": "replace_tile",
|
||
},
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
replace_source = ttp.pil2tensor(Image.new("RGB", (16, 16), (220, 20, 20)))
|
||
replace_masked, _replace_masked_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="source_image",
|
||
mask_blend_mode="off",
|
||
auto_composite_policy="replace_tile",
|
||
source_image=replace_source,
|
||
tile_set=replace_mask_tile_set,
|
||
)
|
||
replace_mask_corner = replace_masked.array[0, 4, 4]
|
||
replace_mask_center = replace_masked.array[0, 8, 8]
|
||
assert_equal(float(replace_mask_corner[0]) > float(replace_mask_corner[2]), True, "replace_tile must preserve source pixels outside an object mask instead of pasting a rectangle")
|
||
assert_equal(float(replace_mask_center[2]) > float(replace_mask_center[0]), True, "replace_tile must hard replace pixels inside an object mask")
|
||
replace_boxed, _replace_boxed_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="source_image",
|
||
mask_blend_mode="off",
|
||
auto_composite_policy="replace_tile",
|
||
replace_tile_shape="tile_box_first",
|
||
source_image=replace_source,
|
||
tile_set=replace_mask_tile_set,
|
||
)
|
||
replace_box_corner = replace_boxed.array[0, 4, 4]
|
||
replace_box_center = replace_boxed.array[0, 8, 8]
|
||
assert_equal(float(replace_box_corner[2]) > float(replace_box_corner[0]), True, "replace_tile tile_box_first should paste the full tile rectangle even when an object mask exists")
|
||
assert_equal(float(replace_box_center[2]) > float(replace_box_center[0]), True, "replace_tile tile_box_first should still replace the object center")
|
||
|
||
replace_soft_mask_full = Image.new("L", (16, 16), 0)
|
||
ImageDraw.Draw(replace_soft_mask_full).rectangle((4, 4, 11, 11), fill=128)
|
||
replace_soft_mask_data = ttp._ttp_encode_object_mask_data([replace_soft_mask_full], [0], [4, 4, 12, 12])
|
||
replace_soft_mask_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [16, 16],
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [16, 16],
|
||
"tiles": [
|
||
{
|
||
"name": "soft_masked_edit",
|
||
"label": "paint mask edit",
|
||
"core_box": [4, 4, 8, 8],
|
||
"sample_box": [4, 4, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
"object_mask": replace_soft_mask_data,
|
||
"composite_mode": "replace_tile",
|
||
},
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
replace_soft_masked, _replace_soft_masked_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="source_image",
|
||
mask_blend_mode="off",
|
||
auto_composite_policy="replace_tile",
|
||
source_image=replace_source,
|
||
tile_set=replace_soft_mask_tile_set,
|
||
)
|
||
replace_soft_center = replace_soft_masked.array[0, 8, 8]
|
||
assert_equal(float(replace_soft_center[2]) > 0.8, True, "replace_tile mask_first should harden soft mask interiors instead of making the whole edit translucent")
|
||
assert_equal(float(replace_soft_center[0]) < 0.2, True, "replace_tile mask_first should remove source pixels from soft mask interiors")
|
||
|
||
replace_own_mask_full = Image.new("L", (16, 16), 0)
|
||
ImageDraw.Draw(replace_own_mask_full).rectangle((7, 7, 8, 8), fill=255)
|
||
replace_source_mask_full = Image.new("L", (16, 16), 0)
|
||
ImageDraw.Draw(replace_source_mask_full).rectangle((4, 4, 11, 11), fill=255)
|
||
replace_own_mask_data = ttp._ttp_encode_object_mask_data([replace_own_mask_full], [0], [4, 4, 12, 12])
|
||
replace_source_mask_data = ttp._ttp_encode_object_mask_data([replace_source_mask_full], [0], [4, 4, 12, 12])
|
||
replace_own_mask_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"original_size": [16, 16],
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [16, 16],
|
||
"tiles": [
|
||
{
|
||
"name": "own_mask_edit",
|
||
"label": "paint mask edit",
|
||
"core_box": [4, 4, 8, 8],
|
||
"sample_box": [4, 4, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
"object_mask": replace_own_mask_data,
|
||
"object_mask_source": replace_source_mask_data,
|
||
"composite_mode": "replace_tile",
|
||
},
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
replace_own_masked, _replace_own_masked_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="source_image",
|
||
mask_blend_mode="off",
|
||
auto_composite_policy="replace_tile",
|
||
source_image=replace_source,
|
||
tile_set=replace_own_mask_tile_set,
|
||
)
|
||
replace_source_only_pixel = replace_own_masked.array[0, 4, 4]
|
||
replace_own_center_pixel = replace_own_masked.array[0, 8, 8]
|
||
assert_equal(float(replace_source_only_pixel[0]) > float(replace_source_only_pixel[2]), True, "replace_tile mask_first should prefer the tile's own cropped object mask over a broader source mask")
|
||
assert_equal(float(replace_own_center_pixel[2]) > float(replace_own_center_pixel[0]), True, "replace_tile mask_first should still replace pixels inside the tile's own mask")
|
||
|
||
subdivided_mask_full = Image.new("L", (64, 64), 0)
|
||
ImageDraw.Draw(subdivided_mask_full).rectangle((24, 24, 39, 39), fill=255)
|
||
subdivided_mask_data = ttp._ttp_encode_object_mask_data([subdivided_mask_full], [0], [24, 24, 40, 40])
|
||
subdivided_detail_tile = {
|
||
"name": "object_5_second_grid_child",
|
||
"label": "object_5",
|
||
"core_box": [24, 24, 16, 16],
|
||
"sample_box": [8, 8, 48, 48],
|
||
"tile_canvas_size": [48, 48],
|
||
"tile_canvas_box": [0, 0, 48, 48],
|
||
"overlap_edges_px_source": {"left": 16, "right": 16, "top": 16, "bottom": 16},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 4,
|
||
"occlusion_priority": 2000,
|
||
"object_mask": subdivided_mask_data,
|
||
"object_mask_source": subdivided_mask_data,
|
||
}
|
||
subdivided_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [64, 64],
|
||
"tiles": [
|
||
{
|
||
"name": "face_base",
|
||
"label": "face",
|
||
"core_box": [0, 0, 64, 64],
|
||
"sample_box": [0, 0, 64, 64],
|
||
"tile_canvas_size": [64, 64],
|
||
"tile_canvas_box": [0, 0, 64, 64],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
"layer": 1,
|
||
"occlusion_priority": 100,
|
||
},
|
||
subdivided_detail_tile,
|
||
],
|
||
}
|
||
subdivided_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": subdivided_meta,
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (64, 64), (220, 20, 20)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (48, 48), (20, 20, 220)))[0],
|
||
],
|
||
}
|
||
assert_equal(ttp._ttp_tile_area_ratio(subdivided_detail_tile, 64 * 64) < 0.07, True, "subdivided detail ranking should use core area instead of padded sample area")
|
||
subdivided_mask_array = ttp._ttp_tile_object_mask_array(subdivided_detail_tile, 48, 48, "mask_feather", 0)
|
||
assert_equal(0.0 < float(subdivided_mask_array[16, 15, 0]) < 1.0, True, "mask_feather should soften subdivided object masks even when blend is zero")
|
||
strict_subdivided, _strict_subdivided_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="strict_layer",
|
||
tile_set=subdivided_tile_set,
|
||
)
|
||
safe_subdivided, _safe_subdivided_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=subdivided_tile_set,
|
||
)
|
||
strict_subdivided_edge = strict_subdivided.array[0, 32, 24]
|
||
safe_subdivided_edge = safe_subdivided.array[0, 32, 24]
|
||
safe_subdivided_center = safe_subdivided.array[0, 32, 32]
|
||
assert_equal(float(safe_subdivided_edge[0]) > float(strict_subdivided_edge[0]), True, "safe auto should not let a repeatedly subdivided masked tile cut a hard hole in the lower face tile")
|
||
assert_equal(float(safe_subdivided_center[2]) > float(safe_subdivided_center[0]), True, "safe auto should still keep the subdivided detail strong at the mask center")
|
||
|
||
split_body_mask = Image.new("L", (64, 32), 255)
|
||
split_body_source_mask = ttp._ttp_encode_object_mask_data([split_body_mask], [0], [0, 0, 64, 32])
|
||
split_body_left_mask = ttp._ttp_encode_object_mask_data([split_body_mask], [0], [0, 0, 32, 32])
|
||
split_body_right_mask = ttp._ttp_encode_object_mask_data([split_body_mask], [0], [32, 0, 64, 32])
|
||
split_body_left_tile = {
|
||
"name": "body_context_grid_1",
|
||
"label": "person body large context",
|
||
"source": "auto_context",
|
||
"core_box": [0, 0, 32, 32],
|
||
"sample_box": [0, 0, 40, 32],
|
||
"tile_canvas_size": [40, 32],
|
||
"tile_canvas_box": [0, 0, 40, 32],
|
||
"overlap_edges_px_source": {"left": 0, "right": 8, "top": 0, "bottom": 0},
|
||
"blend": 8,
|
||
"importance": 0.35,
|
||
"priority": 10.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
"recommended_composite_mode": "context",
|
||
"object_mask": split_body_left_mask,
|
||
"object_mask_source": split_body_source_mask,
|
||
}
|
||
split_body_right_tile = {
|
||
"name": "body_context_grid_2",
|
||
"label": "person body large context",
|
||
"source": "auto_context",
|
||
"core_box": [32, 0, 32, 32],
|
||
"sample_box": [24, 0, 40, 32],
|
||
"tile_canvas_size": [40, 32],
|
||
"tile_canvas_box": [0, 0, 40, 32],
|
||
"overlap_edges_px_source": {"left": 8, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 8,
|
||
"importance": 0.35,
|
||
"priority": 10.0,
|
||
"layer": 0,
|
||
"occlusion_priority": 0,
|
||
"recommended_composite_mode": "context",
|
||
"object_mask": split_body_right_mask,
|
||
"object_mask_source": split_body_source_mask,
|
||
}
|
||
split_source_mask_array = ttp._ttp_tile_object_mask_array(split_body_left_tile, 40, 32, "mask_feather", 8)
|
||
assert_equal(float(split_source_mask_array[16, 36, 0]) > 0.5, True, "subdivided context masks should use source masks so overlap regions stay available")
|
||
split_body_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [64, 32],
|
||
"tiles": [split_body_left_tile, split_body_right_tile],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (40, 32), (90, 90, 90)))[0],
|
||
ttp.pil2tensor(Image.new("RGB", (40, 32), (90, 90, 90)))[0],
|
||
],
|
||
}
|
||
_split_body_output, split_body_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
base_canvas_mode="black",
|
||
mask_blend_mode="mask_feather",
|
||
auto_composite_policy="safe_auto",
|
||
tile_set=split_body_tile_set,
|
||
)
|
||
split_body_single_weight = float(split_body_weights.array[0, 16, 16, 0])
|
||
split_body_seam_weight = float(split_body_weights.array[0, 16, 32, 0])
|
||
assert_equal(split_body_seam_weight > split_body_single_weight + 0.2, True, "subdivided context mask seams should show real overlap blending")
|
||
|
||
fractional_scale_tile_set = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [3, 1],
|
||
"tiles": [
|
||
{
|
||
"name": f"column_{index}",
|
||
"label": "grid",
|
||
"core_box": [index, 0, 1, 1],
|
||
"sample_box": [index, 0, 1, 1],
|
||
"tile_canvas_size": [1, 1],
|
||
"tile_canvas_box": [0, 0, 1, 1],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 0.0,
|
||
}
|
||
for index in range(3)
|
||
],
|
||
},
|
||
"tile_images": [
|
||
ttp.pil2tensor(Image.new("RGB", (1, 1), (index * 80, index * 80, index * 80)))[0]
|
||
for index in range(3)
|
||
],
|
||
}
|
||
_fractional_output, fractional_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.4,
|
||
use_priority=False,
|
||
base_canvas_mode="black",
|
||
weight_preview_mode="coverage",
|
||
tile_set=fractional_scale_tile_set,
|
||
)
|
||
assert_equal(list(fractional_weights.shape), [1, 1, 4, 3], "fractional output scale should round the full canvas size")
|
||
assert_equal(float(fractional_weights.array.min()), 1.0, "fractional output scale should not leave rounded tile boundary gaps")
|
||
fractional_edges = ttp._ttp_scaled_overlap_edges([1, 0, 3, 1], {"left": 1, "right": 0, "top": 0, "bottom": 0}, 1.4, 5, 1)
|
||
assert_equal(fractional_edges["left"], 2, "fractional overlap edges should use source endpoints instead of independent length rounding")
|
||
|
||
deferred_output, deferred_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
assemble_mode="final_only",
|
||
done=False,
|
||
tile_set=blue_tile_set,
|
||
base_image=red_reference,
|
||
)
|
||
deferred_pixel = deferred_output.array[0, 4, 4]
|
||
assert_equal(float(deferred_pixel[0]) > float(deferred_pixel[2]), True, "final-only assemble should return the base preview before loop completion")
|
||
assert_equal(float(deferred_weights.array.max()), 0.0, "final-only assemble should skip weight drawing before loop completion")
|
||
|
||
forced_final_output, forced_final_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=1.0,
|
||
use_priority=True,
|
||
assemble_mode="always",
|
||
pixel_alignment="edge_match",
|
||
done=False,
|
||
tile_set=blue_tile_set,
|
||
base_image=red_reference,
|
||
)
|
||
forced_pixel = forced_final_output.array[0, 4, 4]
|
||
assert_equal(float(forced_pixel[0]) > float(forced_pixel[2]), True, "pixel alignment should force final-only preview during unfinished loops")
|
||
assert_equal(float(forced_final_weights.array.max()), 0.0, "pixel alignment forced final-only mode should skip intermediate weights")
|
||
|
||
base_scale_meta = {
|
||
"type": "ttp_smart_tile",
|
||
"original_size": [8, 8],
|
||
"tiles": [{
|
||
"name": "large_context",
|
||
"label": "large context tile",
|
||
"core_box": [0, 0, 8, 8],
|
||
"sample_box": [0, 0, 8, 8],
|
||
"tile_canvas_size": [8, 8],
|
||
"tile_canvas_box": [0, 0, 8, 8],
|
||
"overlap_edges_px_source": {"left": 0, "right": 0, "top": 0, "bottom": 0},
|
||
"blend": 0,
|
||
"importance": 1.0,
|
||
"priority": 100.0,
|
||
"layer": 2,
|
||
"occlusion_priority": 100,
|
||
}],
|
||
}
|
||
base_canvas = ttp.pil2tensor(Image.new("RGB", (16, 16), (200, 20, 20)))
|
||
low_res_context = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": base_scale_meta,
|
||
"tile_images": [ttp.pil2tensor(Image.new("RGB", (8, 8), (20, 20, 220)))[0]],
|
||
}
|
||
kept_base, _kept_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=0.0,
|
||
use_priority=True,
|
||
tile_set=low_res_context,
|
||
base_image=base_canvas,
|
||
large_tile_policy="use_if_higher_resolution",
|
||
context_tile_weight=0.0,
|
||
)
|
||
assert_equal(list(kept_base.shape), [1, 16, 16, 3], "base_image should set the inferred assemble output size")
|
||
kept_pixel = kept_base.array[0, 8, 8]
|
||
assert_equal(float(kept_pixel[0]) > 0.75 and float(kept_pixel[2]) < 0.1, True, "low-resolution large tiles should not replace a higher-resolution base canvas")
|
||
|
||
high_res_context = {
|
||
"type": "ttp_smart_tile_set",
|
||
"tile_meta": base_scale_meta,
|
||
"tile_images": [ttp.pil2tensor(Image.new("RGB", (16, 16), (20, 20, 220)))[0]],
|
||
}
|
||
used_high_res, _used_weights = assemble_node.assemble_tiles(
|
||
blend_multiplier=1.0,
|
||
output_scale=0.0,
|
||
use_priority=True,
|
||
tile_set=high_res_context,
|
||
base_image=base_canvas,
|
||
large_tile_policy="use_if_higher_resolution",
|
||
context_tile_weight=0.0,
|
||
)
|
||
used_pixel = used_high_res.array[0, 8, 8]
|
||
assert_equal(float(used_pixel[2]) > 0.75 and float(used_pixel[0]) < 0.1, True, "higher-resolution large tiles should be allowed to replace the base canvas")
|
||
|
||
reference = np.zeros((16, 16, 3), dtype=np.float32)
|
||
for y in range(16):
|
||
for x in range(16):
|
||
reference[y, x, :] = [(x + y) / 30.0, x / 15.0, y / 15.0]
|
||
shifted_region = reference[5:13, 6:14].copy()
|
||
alignment_mask = np.ones((8, 8, 1), dtype=np.float32)
|
||
alignment_weights = np.ones((16, 16, 1), dtype=np.float32)
|
||
dx, dy = ttp._ttp_find_pixel_alignment_offset(
|
||
shifted_region,
|
||
reference,
|
||
alignment_weights,
|
||
4,
|
||
4,
|
||
alignment_mask,
|
||
radius=4,
|
||
mode="mask_edge_match",
|
||
)
|
||
assert_equal((dx, dy), (2, 1), "pixel alignment should find the best local offset from surrounding pixels")
|
||
auto_dx, auto_dy, auto_info = ttp._ttp_find_pixel_alignment_offset_auto(
|
||
shifted_region,
|
||
reference,
|
||
alignment_weights,
|
||
4,
|
||
4,
|
||
alignment_mask,
|
||
radius=4,
|
||
mode="mask_edge_match",
|
||
device_mode="cpu",
|
||
)
|
||
assert_equal((auto_dx, auto_dy), (2, 1), "pixel alignment auto wrapper should preserve the CPU result when forced to CPU")
|
||
assert_equal(auto_info["device"], "cpu", "pixel alignment info should report forced CPU execution")
|
||
|
||
fallback_layout = ttp._ttp_boxes_to_auto_layout([], 900, 600, max_tiles=4, include_background=False)
|
||
fallback_meta = ttp._ttp_parse_smart_tile_layout(fallback_layout, 900, 600)
|
||
assert_equal(len(fallback_meta), 4, "auto fallback should produce a 2x2 layout")
|
||
|
||
print("smoke_smart_tile_grid_crop: ok")
|