820 lines
30 KiB
Python
820 lines
30 KiB
Python
import json
|
|
|
|
from PIL import Image
|
|
from PIL.PngImagePlugin import PngInfo
|
|
|
|
|
|
def _save_jpeg_with_user_comment(path, comment: str, prefix: bytes = b"ASCII\x00\x00\x00"):
|
|
exif = Image.Exif()
|
|
if prefix.startswith(b"UNICODE"):
|
|
payload = comment.encode("utf-16le")
|
|
else:
|
|
payload = comment.encode("utf-8")
|
|
exif[37510] = prefix + payload
|
|
Image.new("RGB", (32, 24), (20, 40, 60)).save(path, exif=exif)
|
|
|
|
|
|
def _save_png_with_prompt(path, prompt, workflow=None):
|
|
pnginfo = PngInfo()
|
|
pnginfo.add_text("prompt", json.dumps(prompt))
|
|
if workflow is not None:
|
|
pnginfo.add_text("workflow", json.dumps(workflow))
|
|
Image.new("RGB", (32, 24), (20, 40, 60)).save(path, pnginfo=pnginfo)
|
|
|
|
|
|
def _simple_prompt_graph(positive="original positive prompt", negative="original negative prompt"):
|
|
return {
|
|
"1": {"class_type": "SaveImage", "inputs": {"images": ["2", 0]}},
|
|
"2": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0]}},
|
|
"3": {
|
|
"class_type": "KSampler",
|
|
"inputs": {
|
|
"positive": ["4", 0],
|
|
"negative": ["5", 0],
|
|
"seed": 111,
|
|
"steps": 12,
|
|
"cfg": 5.5,
|
|
"sampler_name": "euler",
|
|
"scheduler": "normal",
|
|
},
|
|
},
|
|
"4": {"class_type": "CLIPTextEncode", "inputs": {"text": positive}},
|
|
"5": {"class_type": "CLIPTextEncode", "inputs": {"text": negative}},
|
|
}
|
|
|
|
|
|
def _upscale_prompt_graph(reader_image="003.png"):
|
|
return {
|
|
"22": {
|
|
"class_type": "FaceDetailer",
|
|
"inputs": {
|
|
"image": ["24", 0],
|
|
"positive": ["26", 0],
|
|
"negative": ["27", 0],
|
|
},
|
|
},
|
|
"24": {
|
|
"class_type": "UltimateSDUpscale",
|
|
"inputs": {
|
|
"image": ["40", 0],
|
|
"positive": ["26", 0],
|
|
"negative": ["27", 0],
|
|
"seed": 222,
|
|
"steps": 9,
|
|
"cfg": 1.5,
|
|
"sampler_name": "Euler",
|
|
"scheduler": "normal",
|
|
"tile_width": 768,
|
|
"seam_fix_mode": "half tile",
|
|
},
|
|
},
|
|
"26": {"class_type": "CLIPTextEncode", "inputs": {"text": ["37", 0]}},
|
|
"27": {"class_type": "CLIPTextEncode", "inputs": {"text": ["38", 0]}},
|
|
"36": {"class_type": "SmartMetadataReader", "inputs": {"image": reader_image}},
|
|
"37": {"class_type": "ShowText|pysssss", "inputs": {"text": ["36", 2]}},
|
|
"38": {"class_type": "ShowText|pysssss", "inputs": {"text": ["36", 3]}},
|
|
"40": {"class_type": "LoadImage", "inputs": {"image": "source.png"}},
|
|
"90": {
|
|
"class_type": "GeminiChatNode",
|
|
"inputs": {"prompt": "unused Gemini prompt template"},
|
|
},
|
|
"91": {"class_type": "ShowText|pysssss", "inputs": {"text": ["90", 0]}},
|
|
"99": {"class_type": "PreviewImage", "inputs": {"images": ["22", 0]}},
|
|
}
|
|
|
|
|
|
def _upscale_workflow(reader_image="003.png"):
|
|
return {
|
|
"nodes": [
|
|
{
|
|
"id": 36,
|
|
"type": "SmartMetadataReader",
|
|
"inputs": {"image": reader_image},
|
|
"outputs": [
|
|
{"name": "image"},
|
|
{"name": "mask"},
|
|
{"name": "positive"},
|
|
{"name": "negative"},
|
|
{"name": "seed"},
|
|
{"name": "steps"},
|
|
{"name": "cfg"},
|
|
{"name": "width"},
|
|
{"name": "height"},
|
|
{"name": "model_name"},
|
|
{"name": "filename"},
|
|
{"name": "setting"},
|
|
],
|
|
},
|
|
{
|
|
"id": 91,
|
|
"type": "ShowText|pysssss",
|
|
"inputs": {"text": ["90", 0]},
|
|
"widgets_values": ["wrong unused prompt"],
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
def _llm_showtext_upscale_prompt(show_class="ShowText|pysssss", include_cache=True):
|
|
return {
|
|
"22": {
|
|
"class_type": "FaceDetailer",
|
|
"inputs": {"image": ["24", 0], "positive": ["26", 0], "negative": ["27", 0]},
|
|
},
|
|
"24": {
|
|
"class_type": "UltimateSDUpscale",
|
|
"inputs": {
|
|
"image": ["40", 0],
|
|
"positive": ["26", 0],
|
|
"negative": ["27", 0],
|
|
"seed": 222,
|
|
"steps": 9,
|
|
"cfg": 1.5,
|
|
"sampler_name": "Euler",
|
|
"scheduler": "normal",
|
|
},
|
|
},
|
|
"26": {"class_type": "CLIPTextEncode", "inputs": {"text": ["37", 0]}},
|
|
"27": {"class_type": "CLIPTextEncode", "inputs": {"text": "bad hands, low quality"}},
|
|
"37": {"class_type": show_class, "inputs": {"text": ["90", 0]}},
|
|
"40": {"class_type": "LoadImage", "inputs": {"image": "source.png"}},
|
|
"90": {
|
|
"class_type": "GeminiChatNode",
|
|
"inputs": {
|
|
"system_instruction": "system instruction should not enter prompt",
|
|
"prompt": "Gemini prompt template should not enter prompt",
|
|
"api_key": "secret-key",
|
|
"model": "gemini-test-model",
|
|
},
|
|
},
|
|
"91": {"class_type": "ShowText|pysssss", "inputs": {"text": ["90", 0]}},
|
|
"99": {"class_type": "PreviewImage", "inputs": {"images": ["22", 0]}},
|
|
}
|
|
|
|
|
|
def _llm_showtext_workflow(show_class="ShowText|pysssss", cached_text="cached gemini positive prompt"):
|
|
widgets_values = [cached_text] if cached_text else []
|
|
return {
|
|
"nodes": [
|
|
{
|
|
"id": 37,
|
|
"type": show_class,
|
|
"inputs": {"text": ["90", 0]},
|
|
"widgets_values": widgets_values,
|
|
},
|
|
{
|
|
"id": 90,
|
|
"type": "GeminiChatNode",
|
|
"inputs": {
|
|
"system_instruction": "system instruction should not enter prompt",
|
|
"prompt": "Gemini prompt template should not enter prompt",
|
|
"api_key": "secret-key",
|
|
"model": "gemini-test-model",
|
|
},
|
|
"widgets_values": [
|
|
"Gemini prompt template should not enter prompt",
|
|
"system instruction should not enter prompt",
|
|
"secret-key",
|
|
"gemini-test-model",
|
|
],
|
|
},
|
|
{
|
|
"id": 91,
|
|
"type": "ShowText|pysssss",
|
|
"inputs": {"text": ["90", 0]},
|
|
"widgets_values": ["wrong unused prompt"],
|
|
},
|
|
]
|
|
}
|
|
|
|
|
|
def _bundle_for_prompt(prompt, workflow=None, filename="sample.png", width=1200, height=1600):
|
|
from smart_metadata_reader.models import MetadataBundle
|
|
|
|
return MetadataBundle(
|
|
filename=filename,
|
|
width=width,
|
|
height=height,
|
|
prompt_raw=json.dumps(prompt),
|
|
workflow_raw=json.dumps(workflow) if workflow is not None else None,
|
|
parameters_raw=None,
|
|
source_format="ComfyUI prompt/workflow",
|
|
)
|
|
|
|
|
|
def test_read_metadata_extracts_png_text_chunks_and_dimensions(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import read_metadata
|
|
|
|
image_path = tmp_path / "sample.png"
|
|
prompt_graph = {
|
|
"1": {
|
|
"class_type": "CLIPTextEncode",
|
|
"inputs": {"text": "positive trigger"},
|
|
}
|
|
}
|
|
workflow_graph = {
|
|
"nodes": [
|
|
{
|
|
"id": 1,
|
|
"type": "CLIPTextEncode",
|
|
"inputs": {"text": "positive trigger"},
|
|
}
|
|
]
|
|
}
|
|
pnginfo = PngInfo()
|
|
pnginfo.add_text("prompt", json.dumps(prompt_graph))
|
|
pnginfo.add_text("workflow", json.dumps(workflow_graph))
|
|
pnginfo.add_text(
|
|
"parameters",
|
|
(
|
|
"positive\n"
|
|
"Negative prompt: negative\n"
|
|
"Steps: 20, Sampler: Euler, CFG scale: 7, Seed: 123, Size: 512x768"
|
|
),
|
|
)
|
|
|
|
Image.new("RGBA", (64, 32), (255, 0, 0, 128)).save(
|
|
image_path, pnginfo=pnginfo
|
|
)
|
|
|
|
bundle = read_metadata(image_path)
|
|
|
|
assert bundle.filename == "sample.png"
|
|
assert bundle.width == 64
|
|
assert bundle.height == 32
|
|
assert json.loads(bundle.prompt_raw) == prompt_graph
|
|
assert json.loads(bundle.workflow_raw) == workflow_graph
|
|
assert "Negative prompt: negative" in bundle.parameters_raw
|
|
assert bundle.source_format == "ComfyUI prompt/workflow"
|
|
|
|
|
|
def test_load_image_and_mask_returns_batched_image_and_mask_shapes(tmp_path):
|
|
from smart_metadata_reader.image_io import load_image_and_mask
|
|
|
|
image_path = tmp_path / "transparent.png"
|
|
Image.new("RGBA", (10, 12), (10, 20, 30, 128)).save(image_path)
|
|
|
|
image, mask, width, height = load_image_and_mask(image_path)
|
|
|
|
assert width == 10
|
|
assert height == 12
|
|
assert tuple(image.shape) == (1, 12, 10, 3)
|
|
assert tuple(mask.shape) == (1, 12, 10)
|
|
|
|
|
|
def test_parse_standard_a1111_parameters():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
parameters = (
|
|
"masterpiece, character trigger\n"
|
|
"Negative prompt: blurry, low quality\n"
|
|
"Steps: 25, Sampler: Euler a, CFG scale: 7.5, Seed: 12345, "
|
|
"Size: 768x1024, Model: demo_model"
|
|
)
|
|
|
|
result = parse_a1111_parameters(parameters)
|
|
|
|
assert result.positive == "masterpiece, character trigger"
|
|
assert result.negative == "blurry, low quality"
|
|
assert result.steps == 25
|
|
assert result.sampler_name == "Euler a"
|
|
assert result.cfg == 7.5
|
|
assert result.seed == 12345
|
|
assert result.width == 768
|
|
assert result.height == 1024
|
|
assert result.model_name == "demo_model"
|
|
assert result.status_message == "A1111 parameters fallback"
|
|
assert result.partial_result["source_format"] == "A1111 parameters"
|
|
|
|
|
|
def test_parse_a1111_preserves_multiline_positive_and_negative_prompts():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
parameters = (
|
|
"masterpiece, best quality\n"
|
|
"character trigger, detailed eyes\n"
|
|
"Negative prompt: low quality\n"
|
|
"bad hands, extra fingers\n"
|
|
"Steps: 30, Sampler: DPM++ 2M, CFG scale: 6, Seed: 999, Size: 512x768"
|
|
)
|
|
|
|
result = parse_a1111_parameters(parameters)
|
|
|
|
assert result.positive == "masterpiece, best quality\ncharacter trigger, detailed eyes"
|
|
assert result.negative == "low quality\nbad hands, extra fingers"
|
|
assert result.steps == 30
|
|
assert result.seed == 999
|
|
|
|
|
|
def test_parse_a1111_preserves_lora_triggers_nsfw_and_user_tags():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
parameters = (
|
|
"<lora:role_lora:0.8>, role_lora_trigger, nsfw, nude, user handwritten tag\n"
|
|
"Negative prompt: low quality\n"
|
|
"Steps: 20, Sampler: Euler, CFG scale: 7, Seed: 101, Size: 640x960"
|
|
)
|
|
|
|
result = parse_a1111_parameters(parameters)
|
|
|
|
assert result.positive == (
|
|
"<lora:role_lora:0.8>, role_lora_trigger, nsfw, nude, user handwritten tag"
|
|
)
|
|
assert result.negative == "low quality"
|
|
|
|
|
|
def test_parse_a1111_without_negative_prompt_keeps_positive_and_empty_negative():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
parameters = (
|
|
"solo, dramatic lighting\n"
|
|
"Steps: 12, Sampler: Euler, CFG scale: 4.5, Seed: 42, Size: 320x448"
|
|
)
|
|
|
|
result = parse_a1111_parameters(parameters)
|
|
|
|
assert result.positive == "solo, dramatic lighting"
|
|
assert result.negative == ""
|
|
assert result.steps == 12
|
|
assert result.seed == 42
|
|
|
|
|
|
def test_parse_a1111_missing_partial_parameters_uses_defaults():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
result = parse_a1111_parameters("simple prompt only")
|
|
|
|
assert result.positive == "simple prompt only"
|
|
assert result.negative == ""
|
|
assert result.seed == -1
|
|
assert result.steps == 0
|
|
assert result.cfg == 0.0
|
|
assert result.width == 0
|
|
assert result.height == 0
|
|
assert result.model_name == ""
|
|
|
|
|
|
def test_parse_a1111_scheduler_and_model_hash_metadata():
|
|
from smart_metadata_reader.a1111_parser import parse_a1111_parameters
|
|
|
|
parameters = (
|
|
"positive text\n"
|
|
"Negative prompt: negative text\n"
|
|
"Steps: 25, Sampler: DPM++ 2M, Scheduler: Karras, CFG scale: 7.25, "
|
|
"Seed: 123, Size: 768x1024, Model hash: abc123, Model: forge_model"
|
|
)
|
|
|
|
result = parse_a1111_parameters(parameters)
|
|
|
|
assert result.scheduler == "Karras"
|
|
assert result.model_name == "forge_model"
|
|
assert result.partial_result["model_hash"] == "abc123"
|
|
|
|
|
|
def test_parse_comfyui_ultimate_sd_upscale_sampler_like_node():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
from smart_metadata_reader.models import MetadataBundle
|
|
|
|
prompt = {
|
|
"1": {
|
|
"class_type": "CheckpointLoaderSimple",
|
|
"inputs": {"ckpt_name": "base_model.safetensors"},
|
|
},
|
|
"2": {
|
|
"class_type": "LoraLoader",
|
|
"inputs": {
|
|
"model": ["1", 0],
|
|
"clip": ["1", 1],
|
|
"lora_name": "upscale_detail.safetensors",
|
|
"strength_model": 0.5,
|
|
"strength_clip": 0.75,
|
|
},
|
|
},
|
|
"3": {"class_type": "VAELoader", "inputs": {"vae_name": "main_vae.safetensors"}},
|
|
"4": {"class_type": "CLIPTextEncode", "inputs": {"text": "role_lora_trigger, 1girl, nsfw"}},
|
|
"5": {"class_type": "CLIPTextEncode", "inputs": {"text": "low quality"}},
|
|
"6": {
|
|
"class_type": "UltimateSDUpscaleNoUpscale",
|
|
"inputs": {
|
|
"model": ["2", 0],
|
|
"clip": ["2", 1],
|
|
"vae": ["3", 0],
|
|
"positive": ["4", 0],
|
|
"negative": ["5", 0],
|
|
"seed": 221,
|
|
"steps": 9,
|
|
"cfg_scale": 1.5,
|
|
"sampler": "Euler",
|
|
"scheduler_name": "normal",
|
|
"tile_width": 768,
|
|
"seam_fix_mode": "half tile",
|
|
"upscale_model": ["9", 0],
|
|
},
|
|
},
|
|
"7": {"class_type": "SaveImage", "inputs": {"images": ["6", 0]}},
|
|
"9": {"class_type": "UpscaleModelLoader", "inputs": {"model_name": "4x.pth"}},
|
|
}
|
|
bundle = MetadataBundle(
|
|
filename="usdu.png",
|
|
width=4800,
|
|
height=6400,
|
|
prompt_raw=json.dumps(prompt),
|
|
workflow_raw=None,
|
|
parameters_raw=None,
|
|
source_format="ComfyUI prompt/workflow",
|
|
)
|
|
|
|
result = parse_metadata_bundle(bundle)
|
|
|
|
assert result.positive == "role_lora_trigger, 1girl, nsfw"
|
|
assert result.negative == "low quality"
|
|
assert result.seed == 221
|
|
assert result.steps == 9
|
|
assert result.cfg == 1.5
|
|
assert result.sampler_name == "Euler"
|
|
assert result.scheduler == "normal"
|
|
assert result.model_name == "base_model.safetensors"
|
|
assert result.vae_name == "main_vae.safetensors"
|
|
assert [lora.lora_name for lora in result.loras] == ["upscale_detail.safetensors"]
|
|
assert "UltimateSDUpscaleNoUpscale 6" in result.debug_trace
|
|
assert "tile_width" not in result.positive
|
|
assert result.status_message == "OK"
|
|
|
|
|
|
def test_failed_comfyui_parse_includes_output_chain_diagnostics_in_setting():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
from smart_metadata_reader.models import MetadataBundle
|
|
|
|
prompt = {
|
|
"1": {"class_type": "UnsupportedGenerator", "inputs": {"image": ["3", 0]}},
|
|
"2": {"class_type": "SaveImage", "inputs": {"images": ["1", 0]}},
|
|
"3": {"class_type": "LoadImage", "inputs": {"image": "source.png"}},
|
|
}
|
|
bundle = MetadataBundle(
|
|
filename="unsupported.png",
|
|
width=512,
|
|
height=768,
|
|
prompt_raw=json.dumps(prompt),
|
|
workflow_raw=json.dumps({"nodes": []}),
|
|
parameters_raw=None,
|
|
source_format="ComfyUI prompt/workflow",
|
|
)
|
|
|
|
result = parse_metadata_bundle(bundle)
|
|
|
|
assert result.status_message == "FAILED"
|
|
assert "no supported sampler candidate found" in result.debug_trace
|
|
assert "SaveImage 2.images <- UnsupportedGenerator 1" in result.debug_trace
|
|
assert "Failure Reason:" in result.setting
|
|
assert "final output nodes found: SaveImage 2" in result.setting
|
|
|
|
|
|
def test_read_metadata_extracts_ascii_and_unicode_exif_user_comment(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import read_metadata
|
|
|
|
ascii_path = tmp_path / "ascii.jpeg"
|
|
unicode_path = tmp_path / "unicode.jpeg"
|
|
_save_jpeg_with_user_comment(ascii_path, "ASCII prompt\nSteps: 9, Seed: 1")
|
|
_save_jpeg_with_user_comment(
|
|
unicode_path,
|
|
"UNICODE prompt\nSteps: 10, Seed: 2",
|
|
prefix=b"UNICODE\x00",
|
|
)
|
|
|
|
ascii_bundle = read_metadata(ascii_path)
|
|
unicode_bundle = read_metadata(unicode_path)
|
|
|
|
assert ascii_bundle.user_comment_raw == "ASCII prompt\nSteps: 9, Seed: 1"
|
|
assert unicode_bundle.user_comment_raw == "UNICODE prompt\nSteps: 10, Seed: 2"
|
|
assert ascii_bundle.source_format == "EXIF UserComment"
|
|
assert unicode_bundle.source_format == "EXIF UserComment"
|
|
|
|
|
|
def test_exif_user_comment_civitai_metadata_fallback(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
positive = (
|
|
"A green tree frog wearing a tiny straw hat sitting on a lily pad with "
|
|
"a fishing rod made from a twig and spider silk."
|
|
)
|
|
resources = [
|
|
{
|
|
"type": "checkpoint",
|
|
"modelVersionId": 2442439,
|
|
"modelName": "Z Image Turbo",
|
|
"modelVersionName": "Turbo",
|
|
},
|
|
{
|
|
"type": "lora",
|
|
"weight": 0.5,
|
|
"modelVersionId": 2460437,
|
|
"modelName": "Midjourney Luneva Cinematic Lora",
|
|
"modelVersionName": "LORA: R128-5000step",
|
|
},
|
|
{
|
|
"type": "lora",
|
|
"weight": 0.8,
|
|
"modelVersionId": 2515203,
|
|
"modelName": "[ZIT] Detail Slider",
|
|
"modelVersionName": "v1.0",
|
|
},
|
|
]
|
|
civitai_metadata = {
|
|
"workflow": "txt2img",
|
|
"cfgScale": 1,
|
|
"steps": 9,
|
|
"seed": 2214950711,
|
|
"prompt": positive,
|
|
"resources": [
|
|
{"modelVersionId": 2442439, "strength": 1, "type": "Checkpoint"},
|
|
{"modelVersionId": 2460437, "strength": 0.5, "type": "LORA"},
|
|
{"modelVersionId": 2515203, "strength": 0.8, "type": "LORA"},
|
|
],
|
|
}
|
|
user_comment = (
|
|
f"{positive}\n"
|
|
"Steps: 9, Sampler: Euler, CFG scale: 1, Seed: 2214950711, "
|
|
"Size: 832x1216, Model type: Z-Image Turbo, "
|
|
f"Civitai resources: {json.dumps(resources)}, "
|
|
f"Civitai metadata: {json.dumps(civitai_metadata)}"
|
|
)
|
|
image_path = tmp_path / "civitai.jpeg"
|
|
_save_jpeg_with_user_comment(image_path, user_comment, prefix=b"UNICODE\x00")
|
|
|
|
bundle = read_metadata(image_path)
|
|
result = parse_metadata_bundle(bundle)
|
|
|
|
assert bundle.user_comment_raw.startswith("A green tree frog")
|
|
assert result.positive == positive
|
|
assert result.negative == ""
|
|
assert result.seed == 2214950711
|
|
assert result.steps == 9
|
|
assert result.cfg == 1.0
|
|
assert result.sampler_name == "Euler"
|
|
assert result.width == 832
|
|
assert result.height == 1216
|
|
assert result.model_name == "Z Image Turbo"
|
|
assert [(lora.lora_name, lora.strength_model) for lora in result.loras] == [
|
|
("Midjourney Luneva Cinematic Lora", 0.5),
|
|
("[ZIT] Detail Slider", 0.8),
|
|
]
|
|
assert result.partial_result["source_format"] == "EXIF UserComment / Civitai metadata"
|
|
assert result.status_message == "EXIF UserComment / Civitai metadata fallback"
|
|
assert "NO_METADATA" not in result.setting
|
|
|
|
|
|
def test_nested_smart_metadata_reader_outputs_resolve_referenced_image_prompts(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
original_path = tmp_path / "003.png"
|
|
_save_png_with_prompt(original_path, _simple_prompt_graph())
|
|
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(
|
|
upscale_path,
|
|
_upscale_prompt_graph(),
|
|
workflow=_upscale_workflow(),
|
|
)
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == "original positive prompt"
|
|
assert result.negative == "original negative prompt"
|
|
assert result.status_message != "FAILED"
|
|
assert not (result.status_message == "OK" and result.positive == "" and result.negative == "")
|
|
assert "Resolved SmartMetadataReader positive output from referenced image 003.png" in result.debug_trace
|
|
assert "Resolved SmartMetadataReader negative output from referenced image 003.png" in result.debug_trace
|
|
|
|
|
|
def test_unconnected_showtext_workflow_cache_does_not_pollute_nested_reader_prompt(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
original_path = tmp_path / "003.png"
|
|
_save_png_with_prompt(original_path, _simple_prompt_graph())
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(
|
|
upscale_path,
|
|
_upscale_prompt_graph(),
|
|
workflow=_upscale_workflow(),
|
|
)
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == "original positive prompt"
|
|
assert "wrong unused prompt" not in result.positive
|
|
assert "wrong unused prompt" not in result.negative
|
|
|
|
|
|
def test_nested_smart_metadata_reader_falls_back_to_current_output_contract_without_workflow_outputs(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
original_path = tmp_path / "003.png"
|
|
_save_png_with_prompt(original_path, _simple_prompt_graph())
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(upscale_path, _upscale_prompt_graph())
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == "original positive prompt"
|
|
assert result.negative == "original negative prompt"
|
|
|
|
|
|
def test_nested_smart_metadata_reader_prefers_workflow_output_names_over_indices(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
original_path = tmp_path / "003.png"
|
|
_save_png_with_prompt(original_path, _simple_prompt_graph())
|
|
prompt = _upscale_prompt_graph()
|
|
prompt["37"]["inputs"]["text"] = ["36", 3]
|
|
prompt["38"]["inputs"]["text"] = ["36", 2]
|
|
workflow = _upscale_workflow()
|
|
workflow["nodes"][0]["outputs"][2] = {"name": "negative"}
|
|
workflow["nodes"][0]["outputs"][3] = {"name": "positive"}
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(upscale_path, prompt, workflow=workflow)
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == "original positive prompt"
|
|
assert result.negative == "original negative prompt"
|
|
|
|
|
|
def test_nested_smart_metadata_reader_missing_image_returns_partial_unresolved(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(
|
|
upscale_path,
|
|
_upscale_prompt_graph(reader_image="missing.png"),
|
|
workflow=_upscale_workflow(reader_image="missing.png"),
|
|
)
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == ""
|
|
assert result.negative == ""
|
|
assert result.status_message == "PARTIAL"
|
|
assert result.confidence <= 0.6
|
|
assert result.partial_result["unresolved"][0]["class_type"] == "SmartMetadataReader"
|
|
assert result.partial_result["unresolved"][0]["output_index"] == 2
|
|
assert result.partial_result["unresolved"][0]["resolved_output_role"] == "positive"
|
|
assert result.partial_result["unresolved"][0]["referenced_image_filename"] == "missing.png"
|
|
assert "referenced image not found" in result.partial_result["unresolved"][0]["reason"]
|
|
assert "Unresolved:" in result.setting
|
|
|
|
|
|
def test_nested_smart_metadata_reader_rejects_unsafe_image_paths(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
for unsafe_path in ("../secret.png", "C:\\Users\\xxx\\secret.png"):
|
|
upscale_path = tmp_path / f"upscaled-{unsafe_path[0].encode().hex()}.png"
|
|
_save_png_with_prompt(
|
|
upscale_path,
|
|
_upscale_prompt_graph(reader_image=unsafe_path),
|
|
workflow=_upscale_workflow(reader_image=unsafe_path),
|
|
)
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == ""
|
|
assert result.status_message == "PARTIAL"
|
|
assert result.partial_result["unresolved"][0]["class_type"] == "SmartMetadataReader"
|
|
assert "unsafe referenced image path" in result.partial_result["unresolved"][0]["reason"]
|
|
|
|
|
|
def test_nested_smart_metadata_reader_respects_actual_connected_output_role(tmp_path):
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle, read_metadata
|
|
|
|
original_path = tmp_path / "003.png"
|
|
_save_png_with_prompt(original_path, _simple_prompt_graph())
|
|
prompt = _upscale_prompt_graph()
|
|
prompt["37"]["inputs"]["text"] = ["36", 3]
|
|
upscale_path = tmp_path / "upscaled.png"
|
|
_save_png_with_prompt(upscale_path, prompt, workflow=_upscale_workflow())
|
|
|
|
result = parse_metadata_bundle(read_metadata(upscale_path))
|
|
|
|
assert result.positive == "original negative prompt"
|
|
assert "SmartMetadataReader output negative used in positive chain" in result.debug_trace
|
|
|
|
|
|
def test_showtext_cache_from_llm_chain_is_used_without_reading_llm_template():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
result = parse_metadata_bundle(
|
|
_bundle_for_prompt(
|
|
_llm_showtext_upscale_prompt(),
|
|
workflow=_llm_showtext_workflow(),
|
|
)
|
|
)
|
|
|
|
assert result.positive == "cached gemini positive prompt"
|
|
assert result.negative == "bad hands, low quality"
|
|
assert "Gemini prompt template" not in result.positive
|
|
assert "system instruction" not in result.positive
|
|
assert "secret-key" not in result.positive
|
|
assert result.status_message != "FAILED"
|
|
assert "using cached ShowText text" in result.debug_trace
|
|
assert "wrong unused prompt" not in result.positive
|
|
|
|
|
|
def test_showtext_prompt_text_0_on_final_chain_beats_llm_template():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
prompt = _llm_showtext_upscale_prompt()
|
|
prompt["37"]["inputs"]["text_0"] = "cached final chain prompt"
|
|
|
|
result = parse_metadata_bundle(_bundle_for_prompt(prompt))
|
|
|
|
assert result.positive == "cached final chain prompt"
|
|
assert "Gemini prompt template" not in result.positive
|
|
assert "system instruction" not in result.positive
|
|
assert result.status_message != "FAILED"
|
|
|
|
|
|
def test_showtext_cache_compatible_class_from_llm_chain_is_used():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
result = parse_metadata_bundle(
|
|
_bundle_for_prompt(
|
|
_llm_showtext_upscale_prompt(show_class="ShowText"),
|
|
workflow=_llm_showtext_workflow(show_class="ShowText"),
|
|
)
|
|
)
|
|
|
|
assert result.positive == "cached gemini positive prompt"
|
|
assert "Gemini prompt template" not in result.positive
|
|
|
|
|
|
def test_showtext_cache_is_single_segment_inside_string_function_chain():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
prompt = _llm_showtext_upscale_prompt()
|
|
prompt["26"]["inputs"]["text"] = ["39", 0]
|
|
prompt["39"] = {
|
|
"class_type": "StringFunction|pysssss",
|
|
"inputs": {
|
|
"action": "append",
|
|
"text_a": "manual prompt",
|
|
"text_b": ["37", 0],
|
|
"text_c": "preset prompt",
|
|
},
|
|
}
|
|
|
|
result = parse_metadata_bundle(
|
|
_bundle_for_prompt(prompt, workflow=_llm_showtext_workflow())
|
|
)
|
|
|
|
assert "manual prompt" in result.positive
|
|
assert "cached gemini positive prompt" in result.positive
|
|
assert "preset prompt" in result.positive
|
|
assert "Gemini prompt template" not in result.positive
|
|
assert "system instruction" not in result.positive
|
|
assert result.positive.index("manual prompt") < result.positive.index("cached gemini positive prompt")
|
|
assert result.positive.index("cached gemini positive prompt") < result.positive.index("preset prompt")
|
|
|
|
|
|
def test_conditioning_combine_showtext_cache_does_not_override_other_branches():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
prompt = _llm_showtext_upscale_prompt()
|
|
prompt["24"]["inputs"]["positive"] = ["41", 0]
|
|
prompt["41"] = {
|
|
"class_type": "ConditioningCombine",
|
|
"inputs": {"conditioning_1": ["42", 0], "conditioning_2": ["43", 0]},
|
|
}
|
|
prompt["42"] = {"class_type": "CLIPTextEncode", "inputs": {"text": "branch A manual prompt"}}
|
|
prompt["43"] = {"class_type": "CLIPTextEncode", "inputs": {"text": ["37", 0]}}
|
|
|
|
result = parse_metadata_bundle(
|
|
_bundle_for_prompt(prompt, workflow=_llm_showtext_workflow())
|
|
)
|
|
|
|
assert "branch A manual prompt" in result.positive
|
|
assert "cached gemini positive prompt" in result.positive
|
|
assert "Gemini prompt template" not in result.positive
|
|
|
|
|
|
def test_showtext_missing_cache_with_llm_upstream_returns_partial_unresolved():
|
|
from smart_metadata_reader.metadata_reader import parse_metadata_bundle
|
|
|
|
result = parse_metadata_bundle(
|
|
_bundle_for_prompt(
|
|
_llm_showtext_upscale_prompt(include_cache=False),
|
|
workflow=_llm_showtext_workflow(cached_text=""),
|
|
)
|
|
)
|
|
|
|
assert result.positive == ""
|
|
assert "Gemini prompt template" not in result.positive
|
|
assert "system instruction" not in result.positive
|
|
assert result.status_message == "PARTIAL"
|
|
assert result.confidence <= 0.6
|
|
assert result.partial_result["unresolved"][0]["class_type"] == "ShowText|pysssss"
|
|
reason = result.partial_result["unresolved"][0]["reason"]
|
|
assert "ShowText cache missing on final chain" in reason
|
|
assert "upstream LLM runtime output was not embedded in metadata" in reason
|
|
assert "Other ShowText caches outside the final chain are ignored" in reason
|
|
assert "wrong unused prompt" not in result.positive
|
|
assert "Unresolved:" in result.setting
|