Retune V10 style transfer recipe
This commit is contained in:
@@ -374,7 +374,7 @@ Values are the 2026-07 render-retuned set.
|
||||
| `keep the same subject` | identity | normal | 1.0 | 1.0 | stack | stack | preserve | 1.0 | 1.0 | — | 1.0 | 1.0 | even |
|
||||
| `copy pose and layout` | composition | grayscale blur | 0.0 | 0.25 | stack | stack | avoid | 1.2 | 0.2 | 1.25 | 1.3 | 0.3 | even |
|
||||
| `copy lighting and mood` | lighting | palette wash | 0.85 | 0.15 | 256 | stack | avoid | 1.0 | 0.55 | 1.25 | 0.8 | 1.3 | lighting |
|
||||
| `suggest the visual style` | style | palette wash | 0.85 | 0.0 | 256 | stack | avoid | 0.85 | 0.85 | 0.9 | 0.8 | 1.85 | style |
|
||||
| `suggest the visual style` | style | strong blur | 1.0 | 0.3 | 384 | stack | avoid | 0.85 | 0.85 | 0.65 | 0.85 | 1.85 | style-transfer |
|
||||
| `suggest material or texture` | material | palette wash | 0.8 | 0.1 | 256 | stack | avoid | 0.5 | 0.75 | 0.95 | 1.0 | 1.55 | material |
|
||||
| `copy big shapes only` | shape only | shape wash | 0.0 | 0.0 | 256 | stack | avoid | 1.1 | 0.0 | 1.0 | 1.2 | 0.05 | even |
|
||||
| `avoid copying text/logos` | text/logo safe | shape wash | 0.0 | 0.0 | 256 | stack | avoid | 0.75 | 0.0 | **0.03** | 0.08 | 0.0 | flat 0.15 |
|
||||
@@ -396,7 +396,9 @@ text/logo safe 0.08/0.0.
|
||||
|
||||
```
|
||||
even: [1.0] × 12 # balanced, identity, composition, framing, shape only
|
||||
style: [0.25, 0.35, 0.45, 0.6, 0.8, 1.0, 1.0, 2.5, 5.0, 1.1, 4.0, 1.2] # style, environment, loose
|
||||
style: [0.25, 0.35, 0.45, 0.6, 0.8, 1.0, 1.0, 2.5, 5.0, 1.1, 4.0, 1.2] # environment, loose
|
||||
style-transfer:
|
||||
[0.062,0.087,0.113,0.15, 0.2, 0.25,1.25,3.438, 6.875, 1.375, 5.5, 1.5] # visual style
|
||||
palette: [0.15, 0.2, 0.3, 0.45, 0.7, 1.0, 1.0, 2.8, 5.5, 1.3, 4.5, 1.2]
|
||||
material: [0.2, 0.3, 0.45, 0.65, 0.85, 1.0, 1.0, 2.0, 4.0, 1.2, 3.0, 1.1]
|
||||
lighting: [0.2, 0.25, 0.35, 0.5, 0.8, 1.0, 1.0, 2.2, 4.5, 1.4, 4.0, 1.2]
|
||||
|
||||
@@ -11,6 +11,9 @@ number, render it, and get a scored verdict from images."
|
||||
| File | Role | Who runs it |
|
||||
| --- | --- | --- |
|
||||
| [tweak_test.py](tweak_test.py) | Render engine: given a recipe (built-in label or a full tweak bundle) + reference + prompt + strength + seed, renders on the ComfyUI box and prints `{name, image, metrics}` as JSON. Pure mechanical work, no judgment. | a cheap agent (Haiku), or you directly |
|
||||
| [style_transfer_stack_test.py](style_transfer_stack_test.py) | Multi-card V10 style-transfer harness: content anchor + style reference + optional away-layout guard. Used to test the repeatable style-transfer method in [v10-style-transfer-method.md](v10-style-transfer-method.md). | maintainer |
|
||||
| [style_transfer_reliability_suite.py](style_transfer_reliability_suite.py) | Fixed V10 style-transfer regression matrix: six content/style stress cases across the default and leak-control methods, with JSONL metrics and a contact sheet. | maintainer |
|
||||
| [style_transfer_seed_sweep.py](style_transfer_seed_sweep.py) | Seed-stability check for the recommended V10 style-transfer methods: three stress cases, two methods, three seeds each, with JSONL metrics and a contact sheet. | maintainer |
|
||||
| the `recipe-tweak-test` workflow | Orchestrates: for each recipe, Haiku agents render the current settings + candidate tweaks in parallel, then a Fable agent **reads the images and scores** each against the recipe's stated intent, names the best, and recommends a change. | the Workflow tool |
|
||||
| [generate_guide_demos.py](generate_guide_demos.py) | Renders the V10 user-guide demo set (every recipe + the journeys) with the matching drag-in V10 workflow embedded in each PNG, and rebuilds the guide manifest + recipe-gallery contact sheet in `docs/assets/krea-v10/demos/`. `--only <slugs>` re-renders a subset; `--skip-render` rebuilds manifest/sheet only. | maintainer |
|
||||
| refs/ | Local copies of reference images so the judge agent can see them. | - |
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Run the V10 style-transfer method reliability suite.
|
||||
|
||||
The suite renders a fixed matrix of content/style stress cases and method
|
||||
variants, writes JSONL metrics, and builds a contact sheet. It intentionally
|
||||
uses the real V10 ComfyUI nodes through style_transfer_stack_test.py.
|
||||
|
||||
Examples:
|
||||
python docs/recipe-lab/style_transfer_reliability_suite.py --render --skip-existing
|
||||
python docs/recipe-lab/style_transfer_reliability_suite.py --sheet-only
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
RUNS = REPO / "docs" / "recipe-lab" / "runs"
|
||||
LOCAL_REFS = Path(r"C:\Users\kgilp\AppData\Local\Temp\krea-style-variety-refs-20260705")
|
||||
STACK_TEST = REPO / "docs" / "recipe-lab" / "style_transfer_stack_test.py"
|
||||
STYLE_RECIPE = REPO / "docs" / "recipe-lab" / "runs" / "codex-style-transfer-final.json"
|
||||
|
||||
SUITE_ID = "20260705"
|
||||
METHODS = ["content-only", "two-card", "final-style", "final-style-guarded"]
|
||||
|
||||
CASES = [
|
||||
{
|
||||
"key": "hare-starry",
|
||||
"label": "Hare -> Starry",
|
||||
"content_ref": "codex-style-variety-20260705/durer_hare.jpg",
|
||||
"content_local": LOCAL_REFS / "durer_hare.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/starry_night.jpg",
|
||||
"style_local": LOCAL_REFS / "starry_night.jpg",
|
||||
"prompt": "the same hare from the content reference sitting on a simple stone plinth in a clean studio scene, no readable text",
|
||||
"seed": 771201,
|
||||
},
|
||||
{
|
||||
"key": "bootprint-hokusai",
|
||||
"label": "Bootprint -> Hokusai",
|
||||
"content_ref": "codex-style-variety-20260705/apollo_bootprint.jpg",
|
||||
"content_local": LOCAL_REFS / "apollo_bootprint.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/hokusai_great_wave.jpg",
|
||||
"style_local": LOCAL_REFS / "hokusai_great_wave.jpg",
|
||||
"prompt": "the same lunar bootprint impression from the content reference in a clean square composition, no readable text",
|
||||
"seed": 771202,
|
||||
},
|
||||
{
|
||||
"key": "figure-mondrian",
|
||||
"label": "Figure -> Mondrian",
|
||||
"content_ref": "codex-style-variety-20260705/vitruvian.jpg",
|
||||
"content_local": LOCAL_REFS / "vitruvian.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/mondrian_color_fields.jpg",
|
||||
"style_local": LOCAL_REFS / "mondrian_color_fields.jpg",
|
||||
"prompt": "the same standing human figure study from the content reference, centered in a clean studio composition, no readable text",
|
||||
"seed": 771203,
|
||||
},
|
||||
{
|
||||
"key": "portrait-starry",
|
||||
"label": "Portrait -> Starry",
|
||||
"content_ref": "codex-style-variety-20260705/migrant_mother.jpg",
|
||||
"content_local": LOCAL_REFS / "migrant_mother.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/starry_night.jpg",
|
||||
"style_local": LOCAL_REFS / "starry_night.jpg",
|
||||
"prompt": "the same adult woman from the content reference in a simple seated studio portrait, no readable text",
|
||||
"seed": 771204,
|
||||
},
|
||||
{
|
||||
"key": "nebula-mondrian",
|
||||
"label": "Nebula -> Mondrian",
|
||||
"content_ref": "codex-style-variety-20260705/pillars_creation.jpg",
|
||||
"content_local": LOCAL_REFS / "pillars_creation.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/mondrian_color_fields.jpg",
|
||||
"style_local": LOCAL_REFS / "mondrian_color_fields.jpg",
|
||||
"prompt": "the same pillar-shaped nebula forms from the content reference in a clean square space image, no readable text",
|
||||
"seed": 771205,
|
||||
},
|
||||
{
|
||||
"key": "hare-hokusai",
|
||||
"label": "Hare -> Hokusai",
|
||||
"content_ref": "codex-style-variety-20260705/durer_hare.jpg",
|
||||
"content_local": LOCAL_REFS / "durer_hare.jpg",
|
||||
"style_ref": "codex-style-variety-20260705/hokusai_great_wave.jpg",
|
||||
"style_local": LOCAL_REFS / "hokusai_great_wave.jpg",
|
||||
"prompt": "the same hare from the content reference sitting on a simple stone plinth in a clean studio scene, no readable text",
|
||||
"seed": 771206,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def output_name(case_key, method):
|
||||
return f"codex-suite-{case_key}-{method}-{SUITE_ID}"
|
||||
|
||||
|
||||
def output_path(case_key, method):
|
||||
return RUNS / f"{output_name(case_key, method)}.png"
|
||||
|
||||
|
||||
def run_render(case, method, skip_existing):
|
||||
image = output_path(case["key"], method)
|
||||
if skip_existing and image.exists():
|
||||
return None
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(STACK_TEST),
|
||||
"--method",
|
||||
method,
|
||||
"--content-ref",
|
||||
case["content_ref"],
|
||||
"--style-ref",
|
||||
case["style_ref"],
|
||||
"--style-recipe-json",
|
||||
str(STYLE_RECIPE),
|
||||
"--prompt",
|
||||
case["prompt"],
|
||||
"--seed",
|
||||
str(case["seed"]),
|
||||
"--name",
|
||||
output_name(case["key"], method),
|
||||
"--balance",
|
||||
]
|
||||
proc = subprocess.run(cmd, cwd=REPO, check=True, capture_output=True, text=True)
|
||||
lines = [line for line in proc.stdout.splitlines() if line.strip()]
|
||||
result = json.loads(lines[-1])
|
||||
result["case"] = case["key"]
|
||||
return result
|
||||
|
||||
|
||||
def write_metrics(records):
|
||||
out = RUNS / f"codex-style-transfer-reliability-suite-{SUITE_ID}.jsonl"
|
||||
with out.open("w", encoding="utf-8") as f:
|
||||
for record in records:
|
||||
f.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
return out
|
||||
|
||||
|
||||
def build_sheet():
|
||||
out = RUNS / f"codex-style-transfer-reliability-suite-{SUITE_ID}.png"
|
||||
cols = [("Content ref", "content"), ("Style ref", "style")] + [(m, m) for m in METHODS]
|
||||
|
||||
try:
|
||||
title_font = ImageFont.truetype(r"C:\Windows\Fonts\arialbd.ttf", 21)
|
||||
header_font = ImageFont.truetype(r"C:\Windows\Fonts\arialbd.ttf", 13)
|
||||
label_font = ImageFont.truetype(r"C:\Windows\Fonts\arial.ttf", 12)
|
||||
except Exception:
|
||||
title_font = header_font = label_font = ImageFont.load_default()
|
||||
|
||||
thumb = 146
|
||||
label_h = 38
|
||||
header_h = 104
|
||||
margin = 14
|
||||
gap = 9
|
||||
sheet_w = margin * 2 + len(cols) * thumb + (len(cols) - 1) * gap
|
||||
sheet_h = header_h + margin + len(CASES) * (thumb + label_h) + (len(CASES) - 1) * gap + margin
|
||||
sheet = Image.new("RGB", (sheet_w, sheet_h), "#f4f1ea")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
draw.text((margin, 12), "V10 style-transfer reliability suite", fill="#171411", font=title_font)
|
||||
draw.text(
|
||||
(margin, 42),
|
||||
"Fixed public-domain content/style stress cases. Compare default transfer against safer final-detail variants.",
|
||||
fill="#4a443d",
|
||||
font=label_font,
|
||||
)
|
||||
draw.text(
|
||||
(margin, 62),
|
||||
"Methods: content-only baseline, two-card default, final-style leak control, final-style-guarded composition guard.",
|
||||
fill="#4a443d",
|
||||
font=label_font,
|
||||
)
|
||||
|
||||
for col_index, (heading, _kind) in enumerate(cols):
|
||||
x = margin + col_index * (thumb + gap)
|
||||
draw.text((x, header_h - 26), heading, fill="#171411", font=header_font)
|
||||
|
||||
for row_index, case in enumerate(CASES):
|
||||
y = header_h + margin + row_index * (thumb + label_h + gap)
|
||||
for col_index, (_heading, kind) in enumerate(cols):
|
||||
x = margin + col_index * (thumb + gap)
|
||||
if kind == "content":
|
||||
path = case["content_local"]
|
||||
caption = case["label"]
|
||||
elif kind == "style":
|
||||
path = case["style_local"]
|
||||
caption = "style source"
|
||||
else:
|
||||
path = output_path(case["key"], kind)
|
||||
caption = kind
|
||||
img = Image.open(path).convert("RGB")
|
||||
img = ImageOps.contain(img, (thumb, thumb), Image.Resampling.LANCZOS)
|
||||
frame = Image.new("RGB", (thumb, thumb), "#ded7cb")
|
||||
frame.paste(img, ((thumb - img.width) // 2, (thumb - img.height) // 2))
|
||||
sheet.paste(frame, (x, y))
|
||||
draw.rectangle((x, y, x + thumb - 1, y + thumb - 1), outline="#b7aea1", width=1)
|
||||
draw.text((x, y + thumb + 7), caption, fill="#28231e", font=label_font)
|
||||
sheet.save(out, quality=94)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--render", action="store_true", help="render every case/method image")
|
||||
ap.add_argument("--skip-existing", action="store_true", help="do not rerender existing PNGs")
|
||||
ap.add_argument("--sheet-only", action="store_true", help="only rebuild the contact sheet")
|
||||
args = ap.parse_args()
|
||||
|
||||
RUNS.mkdir(parents=True, exist_ok=True)
|
||||
records = []
|
||||
if args.render and not args.sheet_only:
|
||||
for case in CASES:
|
||||
for method in METHODS:
|
||||
print(f"Rendering {case['key']} / {method}", flush=True)
|
||||
record = run_render(case, method, args.skip_existing)
|
||||
if record is not None:
|
||||
records.append(record)
|
||||
if records:
|
||||
metrics_path = write_metrics(records)
|
||||
print(metrics_path)
|
||||
sheet_path = build_sheet()
|
||||
print(sheet_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Run a seed-sweep check for the V10 style-transfer method.
|
||||
|
||||
The broad reliability suite varies content/style cases. This one varies seeds
|
||||
for the recommended methods so we can see whether the method is stable across
|
||||
sampling noise, not just lucky on one seed.
|
||||
|
||||
Examples:
|
||||
python docs/recipe-lab/style_transfer_seed_sweep.py --render --skip-existing
|
||||
python docs/recipe-lab/style_transfer_seed_sweep.py --sheet-only
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import style_transfer_reliability_suite as suite
|
||||
|
||||
|
||||
RUNS = suite.RUNS
|
||||
REPO = suite.REPO
|
||||
STACK_TEST = suite.STACK_TEST
|
||||
STYLE_RECIPE = suite.STYLE_RECIPE
|
||||
SUITE_ID = suite.SUITE_ID
|
||||
|
||||
CASE_KEYS = ["hare-starry", "bootprint-hokusai", "figure-mondrian"]
|
||||
METHODS = ["two-card", "final-style"]
|
||||
SEEDS = [771301, 771302, 771303]
|
||||
CASES = [case for case in suite.CASES if case["key"] in CASE_KEYS]
|
||||
|
||||
|
||||
def output_name(case_key, method, seed):
|
||||
return f"codex-seedsweep-{case_key}-{method}-seed{seed}-{SUITE_ID}"
|
||||
|
||||
|
||||
def output_path(case_key, method, seed):
|
||||
return RUNS / f"{output_name(case_key, method, seed)}.png"
|
||||
|
||||
|
||||
def run_render(case, method, seed, skip_existing):
|
||||
image = output_path(case["key"], method, seed)
|
||||
if skip_existing and image.exists():
|
||||
return None
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(STACK_TEST),
|
||||
"--method",
|
||||
method,
|
||||
"--content-ref",
|
||||
case["content_ref"],
|
||||
"--style-ref",
|
||||
case["style_ref"],
|
||||
"--style-recipe-json",
|
||||
str(STYLE_RECIPE),
|
||||
"--prompt",
|
||||
case["prompt"],
|
||||
"--seed",
|
||||
str(seed),
|
||||
"--name",
|
||||
output_name(case["key"], method, seed),
|
||||
"--balance",
|
||||
]
|
||||
proc = subprocess.run(cmd, cwd=REPO, check=True, capture_output=True, text=True)
|
||||
lines = [line for line in proc.stdout.splitlines() if line.strip()]
|
||||
result = json.loads(lines[-1])
|
||||
result["case"] = case["key"]
|
||||
result["seed"] = seed
|
||||
return result
|
||||
|
||||
|
||||
def write_metrics(records):
|
||||
out = RUNS / f"codex-style-transfer-seed-sweep-{SUITE_ID}.jsonl"
|
||||
with out.open("w", encoding="utf-8") as f:
|
||||
for record in records:
|
||||
f.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
return out
|
||||
|
||||
|
||||
def build_sheet():
|
||||
out = RUNS / f"codex-style-transfer-seed-sweep-{SUITE_ID}.png"
|
||||
columns = [("Content ref", "content", None), ("Style ref", "style", None)]
|
||||
for method in METHODS:
|
||||
for seed in SEEDS:
|
||||
columns.append((f"{method}\n{seed}", method, seed))
|
||||
|
||||
try:
|
||||
title_font = ImageFont.truetype(r"C:\Windows\Fonts\arialbd.ttf", 21)
|
||||
header_font = ImageFont.truetype(r"C:\Windows\Fonts\arialbd.ttf", 12)
|
||||
label_font = ImageFont.truetype(r"C:\Windows\Fonts\arial.ttf", 12)
|
||||
except Exception:
|
||||
title_font = header_font = label_font = ImageFont.load_default()
|
||||
|
||||
thumb = 132
|
||||
label_h = 38
|
||||
header_h = 112
|
||||
margin = 14
|
||||
gap = 8
|
||||
sheet_w = margin * 2 + len(columns) * thumb + (len(columns) - 1) * gap
|
||||
sheet_h = header_h + margin + len(CASES) * (thumb + label_h) + (len(CASES) - 1) * gap + margin
|
||||
sheet = Image.new("RGB", (sheet_w, sheet_h), "#f4f1ea")
|
||||
draw = ImageDraw.Draw(sheet)
|
||||
draw.text((margin, 12), "V10 style-transfer seed sweep", fill="#171411", font=title_font)
|
||||
draw.text(
|
||||
(margin, 42),
|
||||
"Three seeds per recommended method. Failures here mean the method is seed-lucky rather than reliable.",
|
||||
fill="#4a443d",
|
||||
font=label_font,
|
||||
)
|
||||
draw.text(
|
||||
(margin, 62),
|
||||
"two-card = strongest visible transfer; final-style = leak-control fallback.",
|
||||
fill="#4a443d",
|
||||
font=label_font,
|
||||
)
|
||||
|
||||
for col_index, (heading, _method, _seed) in enumerate(columns):
|
||||
x = margin + col_index * (thumb + gap)
|
||||
for line_index, line in enumerate(heading.splitlines()):
|
||||
draw.text((x, header_h - 40 + line_index * 16), line, fill="#171411", font=header_font)
|
||||
|
||||
for row_index, case in enumerate(CASES):
|
||||
y = header_h + margin + row_index * (thumb + label_h + gap)
|
||||
for col_index, (_heading, method, seed) in enumerate(columns):
|
||||
x = margin + col_index * (thumb + gap)
|
||||
if method == "content":
|
||||
path = case["content_local"]
|
||||
caption = case["label"]
|
||||
elif method == "style":
|
||||
path = case["style_local"]
|
||||
caption = "style source"
|
||||
else:
|
||||
path = output_path(case["key"], method, seed)
|
||||
caption = f"{method} {seed}"
|
||||
img = Image.open(path).convert("RGB")
|
||||
img = ImageOps.contain(img, (thumb, thumb), Image.Resampling.LANCZOS)
|
||||
frame = Image.new("RGB", (thumb, thumb), "#ded7cb")
|
||||
frame.paste(img, ((thumb - img.width) // 2, (thumb - img.height) // 2))
|
||||
sheet.paste(frame, (x, y))
|
||||
draw.rectangle((x, y, x + thumb - 1, y + thumb - 1), outline="#b7aea1", width=1)
|
||||
draw.text((x, y + thumb + 7), caption, fill="#28231e", font=label_font)
|
||||
sheet.save(out, quality=94)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--render", action="store_true", help="render every case/method/seed image")
|
||||
ap.add_argument("--skip-existing", action="store_true", help="do not rerender existing PNGs")
|
||||
ap.add_argument("--sheet-only", action="store_true", help="only rebuild the contact sheet")
|
||||
args = ap.parse_args()
|
||||
|
||||
RUNS.mkdir(parents=True, exist_ok=True)
|
||||
records = []
|
||||
if args.render and not args.sheet_only:
|
||||
for case in CASES:
|
||||
for method in METHODS:
|
||||
for seed in SEEDS:
|
||||
print(f"Rendering {case['key']} / {method} / {seed}", flush=True)
|
||||
record = run_render(case, method, seed, args.skip_existing)
|
||||
if record is not None:
|
||||
records.append(record)
|
||||
if records:
|
||||
metrics_path = write_metrics(records)
|
||||
print(metrics_path)
|
||||
sheet_path = build_sheet()
|
||||
print(sheet_path)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Render V10 multi-card style-transfer stack experiments.
|
||||
|
||||
This complements tweak_test.py, which is intentionally one-card. The goal here
|
||||
is to test a *method*: keep a content reference stable while applying a style
|
||||
reference through V10 guide cards.
|
||||
|
||||
Examples:
|
||||
python docs/recipe-lab/style_transfer_stack_test.py --method two-card \
|
||||
--content-ref codex-style-variety-20260705/durer_hare.jpg \
|
||||
--style-ref codex-style-variety-20260705/starry_night.jpg \
|
||||
--style-recipe-json docs/recipe-lab/runs/codex-style-transfer-final.json \
|
||||
--prompt "the same hare sitting on a stone plinth, no readable text" \
|
||||
--seed 771201 --name hare-starry-two-card
|
||||
|
||||
Prints one JSON line with local output path and metrics against both refs.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import tweak_test as tt
|
||||
|
||||
|
||||
def stack_inputs(prompt, prompt_strength, balance):
|
||||
return {
|
||||
"Krea CLIP": ["clip", 0],
|
||||
"Final image prompt": prompt,
|
||||
"Written prompt strength": prompt_strength,
|
||||
"Image slider feel": "artist friendly - soft at low values",
|
||||
"Image detail level": "medium - balanced default (384)",
|
||||
"Image framing": "keep full image shape",
|
||||
"When images guide": "smart per-card timing",
|
||||
"Early-to-final handoff": 0.4,
|
||||
"Text/logo guard prompt handling": "gentle guard - keep my prompt words",
|
||||
"Balance strong cards": "gentle balance" if balance else "off - use my values",
|
||||
"Reuse image studies": "always re-study",
|
||||
}
|
||||
|
||||
|
||||
def method_cards(method, style_label):
|
||||
"""Return card specs for one V10 style-transfer method."""
|
||||
if method == "content-only":
|
||||
return [
|
||||
{"ref": "content", "recipe": "keep the same subject", "strength": 0.9},
|
||||
]
|
||||
if method == "style-only":
|
||||
return [
|
||||
{"ref": "style", "recipe": style_label, "strength": 0.65},
|
||||
]
|
||||
if method == "two-card":
|
||||
return [
|
||||
{"ref": "content", "recipe": "keep the same subject", "strength": 0.9},
|
||||
{"ref": "style", "recipe": style_label, "strength": 0.55},
|
||||
]
|
||||
if method == "final-style":
|
||||
return [
|
||||
{"ref": "content", "recipe": "keep the same subject", "strength": 0.9},
|
||||
{
|
||||
"ref": "style",
|
||||
"recipe": style_label,
|
||||
"strength": 0.65,
|
||||
"timing": "final details only",
|
||||
},
|
||||
]
|
||||
if method == "final-style-guarded":
|
||||
return [
|
||||
{"ref": "content", "recipe": "keep the same subject", "strength": 0.9},
|
||||
{
|
||||
"ref": "style",
|
||||
"recipe": style_label,
|
||||
"strength": 0.65,
|
||||
"timing": "final details only",
|
||||
},
|
||||
{
|
||||
"ref": "style",
|
||||
"recipe": "copy pose and layout",
|
||||
"strength": 0.1,
|
||||
"direction": "away from this image",
|
||||
"timing": "early layout only",
|
||||
},
|
||||
]
|
||||
if method == "stable-style":
|
||||
return [
|
||||
{"ref": "content", "recipe": "keep the same subject", "strength": 0.9},
|
||||
{
|
||||
"ref": "style",
|
||||
"recipe": style_label,
|
||||
"strength": 0.6,
|
||||
"timing": "final details only",
|
||||
},
|
||||
{
|
||||
"ref": "style",
|
||||
"recipe": "copy pose and layout",
|
||||
"strength": 0.2,
|
||||
"direction": "away from this image",
|
||||
"timing": "early layout only",
|
||||
},
|
||||
]
|
||||
raise ValueError(f"unknown method: {method}")
|
||||
|
||||
|
||||
def build_graph(method, content_ref, style_ref, style_label, prompt, prompt_strength, seed, name, balance):
|
||||
graph = {
|
||||
"unet": {"class_type": "UNETLoader", "inputs": {"unet_name": tt.MODEL, "weight_dtype": "default"}},
|
||||
"sampling": {"class_type": "ModelSamplingAuraFlow", "inputs": {"shift": 3.14, "model": ["unet", 0]}},
|
||||
"clip": {"class_type": "CLIPLoader", "inputs": {"clip_name": tt.CLIP, "type": "krea2", "device": "default"}},
|
||||
"vae": {"class_type": "VAELoader", "inputs": {"vae_name": tt.VAE}},
|
||||
"neg": tt.stack(tt.NEG),
|
||||
"latent": {"class_type": "EmptyLatentImage", "inputs": {"width": 512, "height": 512, "batch_size": 1}},
|
||||
"decode": {"class_type": "VAEDecode", "inputs": {"samples": ["sampler", 0], "vae": ["vae", 0]}},
|
||||
"save": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"images": ["decode", 0], "filename_prefix": "claude-generations/recipe-lab/" + name},
|
||||
},
|
||||
}
|
||||
ref_paths = {"content": content_ref, "style": style_ref}
|
||||
pos = stack_inputs(prompt, prompt_strength, balance)
|
||||
|
||||
for i, spec in enumerate(method_cards(method, style_label), start=1):
|
||||
load_key = f"load{i}"
|
||||
card_key = f"card{i}"
|
||||
graph[load_key] = {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": ref_paths[spec["ref"]], "upload": "image"},
|
||||
}
|
||||
card_inputs = dict(tt.CARD_DEFAULTS)
|
||||
card_inputs.update(
|
||||
{
|
||||
"Reference image": [load_key, 0],
|
||||
"How strongly this image guides": spec["strength"],
|
||||
"Use image for": spec["recipe"],
|
||||
"Guide direction": spec.get("direction", "toward this image"),
|
||||
"When this card guides": spec.get("timing", "recipe decides"),
|
||||
}
|
||||
)
|
||||
graph[card_key] = {"class_type": "KGKrea2ImageGuideCardV10", "inputs": card_inputs}
|
||||
pos[f"Reference {i} guide card"] = [card_key, 0]
|
||||
|
||||
graph["pos"] = {"class_type": "KGTextEncodeKreaImageReferencesV10", "inputs": pos}
|
||||
graph["sampler"] = {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": 8,
|
||||
"cfg": 1.0,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1.0,
|
||||
"model": ["sampling", 0],
|
||||
"positive": ["pos", 0],
|
||||
"negative": ["neg", 0],
|
||||
"latent_image": ["latent", 0],
|
||||
},
|
||||
}
|
||||
return graph
|
||||
|
||||
|
||||
def input_ref(server, path):
|
||||
return tt.fetch(
|
||||
server,
|
||||
{
|
||||
"filename": Path(path).name,
|
||||
"subfolder": str(Path(path).parent).replace("\\", "/"),
|
||||
"type": "input",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def metrics(out, content_img, style_img):
|
||||
return {
|
||||
"content_palette_dist": round(sum(abs(a - b) for a, b in zip(tt.hist(out), tt.hist(content_img))), 3),
|
||||
"content_struct_sim": round(tt.struct_sim(tt.gray(out), tt.gray(content_img)), 3),
|
||||
"style_palette_dist": round(sum(abs(a - b) for a, b in zip(tt.hist(out), tt.hist(style_img))), 3),
|
||||
"style_struct_sim": round(tt.struct_sim(tt.gray(out), tt.gray(style_img)), 3),
|
||||
"texture_energy": round(tt.lapvar(out), 0),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument(
|
||||
"--method",
|
||||
choices=[
|
||||
"content-only",
|
||||
"style-only",
|
||||
"two-card",
|
||||
"final-style",
|
||||
"final-style-guarded",
|
||||
"stable-style",
|
||||
],
|
||||
required=True,
|
||||
)
|
||||
ap.add_argument("--content-ref", required=True)
|
||||
ap.add_argument("--style-ref", required=True)
|
||||
ap.add_argument("--style-recipe-json")
|
||||
ap.add_argument("--style-label", default="suggest the visual style")
|
||||
ap.add_argument("--prompt", required=True)
|
||||
ap.add_argument("--prompt-strength", type=float, default=1.1)
|
||||
ap.add_argument("--seed", type=int, default=771201)
|
||||
ap.add_argument("--name", required=True)
|
||||
ap.add_argument("--server", default=tt.DEFAULT_SERVER)
|
||||
ap.add_argument("--balance", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
server = args.server.rstrip("/")
|
||||
style_label = args.style_label
|
||||
if args.style_recipe_json:
|
||||
bundle = json.loads(Path(args.style_recipe_json).read_text(encoding="utf-8"))
|
||||
style_label = bundle["label"]
|
||||
tt.OLGA_RECIPES.mkdir(parents=True, exist_ok=True)
|
||||
(tt.OLGA_RECIPES / ("lab-" + style_label.replace(" ", "-") + ".json")).write_text(
|
||||
json.dumps(bundle),
|
||||
encoding="utf-8",
|
||||
)
|
||||
tt.refresh_card_info(server)
|
||||
|
||||
info = tt.submit(
|
||||
server,
|
||||
build_graph(
|
||||
args.method,
|
||||
args.content_ref,
|
||||
args.style_ref,
|
||||
style_label,
|
||||
args.prompt,
|
||||
args.prompt_strength,
|
||||
args.seed,
|
||||
args.name,
|
||||
args.balance,
|
||||
),
|
||||
)
|
||||
out = tt.fetch(server, info)
|
||||
tt.OUTDIR.mkdir(parents=True, exist_ok=True)
|
||||
local = tt.OUTDIR / (args.name + ".png")
|
||||
out.save(local)
|
||||
content_img = input_ref(server, args.content_ref)
|
||||
style_img = input_ref(server, args.style_ref)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"name": args.name,
|
||||
"method": args.method,
|
||||
"image": str(local),
|
||||
"style_label": style_label,
|
||||
"metrics": metrics(out, content_img, style_img),
|
||||
}
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -19,7 +19,7 @@ from pathlib import Path
|
||||
from PIL import Image, ImageFilter
|
||||
|
||||
DEFAULT_SERVER = "http://10.0.0.35:8188"
|
||||
OLGA_RECIPES = Path("//Olga/d/ComfyUI/custom_nodes/krea-reference/custom_recipes")
|
||||
OLGA_RECIPES = Path("//Olga/d/ComfyUI/custom_nodes/comfyui-krea-reference/custom_recipes")
|
||||
OUTDIR = Path(__file__).resolve().parent / "runs"
|
||||
MODEL = "krea2\\krea2_turbo_nvfp4.safetensors"
|
||||
CLIP = "krea2\\qwen3vl_4b_fp8_scaled.safetensors"
|
||||
@@ -86,6 +86,12 @@ def fetch(server, info):
|
||||
return Image.open(io.BytesIO(r.read())).convert("RGB")
|
||||
|
||||
|
||||
def refresh_card_info(server):
|
||||
"""Force ComfyUI to rescan V10 card inputs after dropping a lab recipe."""
|
||||
with urllib.request.urlopen(server + "/object_info/KGKrea2ImageGuideCardV10", timeout=30) as r:
|
||||
r.read()
|
||||
|
||||
|
||||
def hist(img, bins=8):
|
||||
img = img.resize((128, 128))
|
||||
h = [0.0] * (bins * 3)
|
||||
@@ -142,6 +148,8 @@ def main():
|
||||
return 2
|
||||
|
||||
server = args.server.rstrip("/")
|
||||
if args.recipe_json:
|
||||
refresh_card_info(server)
|
||||
info = submit(server, build_graph(label, args.ref, args.prompt, args.strength, args.seed, args.name))
|
||||
out = fetch(server, info)
|
||||
local = OUTDIR / (args.name + ".png")
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# V10 Style Transfer Method
|
||||
|
||||
Goal: make style transfer reliable inside the V10 Krea guide-card stack while
|
||||
preserving the requested content subject. This is a stack method, not only a
|
||||
single recipe.
|
||||
|
||||
## Default Method
|
||||
|
||||
Use two guide cards:
|
||||
|
||||
| Slot | Reference | Use image for | Direction | Card timing | Strength |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | content image | `keep the same subject` | toward | recipe decides | `0.9` |
|
||||
| 2 | style image | `suggest the visual style` | toward | recipe decides | `0.55` |
|
||||
|
||||
Stack settings:
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| `When images guide` | `smart per-card timing` |
|
||||
| `Balance strong cards` | `gentle balance` |
|
||||
| `Image slider feel` | `artist friendly - soft at low values` |
|
||||
| `Written prompt strength` | start around `1.1` |
|
||||
|
||||
This is the best first pass when the user wants obvious style transfer. The
|
||||
content card anchors the subject; the style card brings palette, medium, and
|
||||
finish. It is stronger than the single-card style recipe and much more stable
|
||||
than trying to style-transfer from a prompt alone.
|
||||
|
||||
## Input Hygiene
|
||||
|
||||
The content reference must be clean. `keep the same subject` preserves the
|
||||
visible content subject, and if the image contains several people or objects it
|
||||
may preserve several of them. For reliable style transfer, crop or choose a
|
||||
content reference where the intended subject is isolated, centered, and free of
|
||||
extra people, text, logos, or distracting props.
|
||||
|
||||
The style reference can be messier, but source-heavy style images need the
|
||||
leak-control method below. Strong subjects, famous scenes, portraits, big
|
||||
foreground objects, and hard composition lines are the risky cases.
|
||||
|
||||
## Leak-Control Method
|
||||
|
||||
If the style source starts bringing its scene/layout along with the look,
|
||||
change only the style card timing:
|
||||
|
||||
| Slot | Reference | Use image for | Direction | Card timing | Strength |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | content image | `keep the same subject` | toward | recipe decides | `0.9` |
|
||||
| 2 | style image | `suggest the visual style` | toward | `final details only` | `0.65` |
|
||||
|
||||
This gives the content image and prompt first claim on layout, then applies the
|
||||
style in the late/detail phase. It is safer for source-heavy artwork, photos,
|
||||
and drawings, but the style can be quieter.
|
||||
|
||||
Use this as the first choice when content fidelity matters more than obvious
|
||||
style intensity, or when the style reference has a strong subject/scene.
|
||||
|
||||
## Optional Layout Guard
|
||||
|
||||
Use this only when the style image still imposes its composition:
|
||||
|
||||
| Slot | Reference | Use image for | Direction | Card timing | Strength |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 3 | style image | `copy pose and layout` | away | `early layout only` | `0.1` |
|
||||
|
||||
The `0.1` guard is intentionally small. A heavier `0.2` away-layout card was
|
||||
render-tested and proved too conservative: it protected content but often
|
||||
muted the useful style signal.
|
||||
|
||||
## Decision Tree
|
||||
|
||||
1. Use the default two-card method when the style source is abstract,
|
||||
texture-like, graphic, or when the user wants obvious visual transformation.
|
||||
2. Use leak-control (`final details only`) when the style source has a strong
|
||||
scene/subject or the first render starts importing the source layout.
|
||||
3. Add the optional `0.1` layout guard only after leak-control still shows
|
||||
source composition leakage.
|
||||
4. Do not start with the guard. It is a correction, not the default.
|
||||
5. If all safe variants look too quiet, return to the default two-card method
|
||||
and accept some background/style-source presence as the tradeoff for a
|
||||
visible transfer.
|
||||
|
||||
## Evidence
|
||||
|
||||
Rendered on the real V10 ComfyUI server using public-domain references:
|
||||
|
||||
- `codex-style-transfer-variety-proof-20260705.png`: one-card style recipe
|
||||
across paintings, drawings, photos, and different prompts.
|
||||
- `codex-style-transfer-method-stack-expanded-proof-20260705.png`: content
|
||||
anchor + style-stack comparisons across animal, object/terrain, and figure
|
||||
cases.
|
||||
- `codex-style-transfer-reliability-suite-20260705.png`: 24-render regression
|
||||
matrix across six content/style stress cases and four methods.
|
||||
- `codex-style-transfer-reliability-suite-20260705.jsonl`: matching metrics
|
||||
against content and style references.
|
||||
- `codex-style-transfer-seed-sweep-20260705.png`: 18-render seed-stability
|
||||
check across the two recommended methods.
|
||||
- `codex-style-transfer-seed-sweep-20260705.jsonl`: matching seed-sweep
|
||||
metrics.
|
||||
|
||||
Key observations from the expanded method proof:
|
||||
|
||||
- `style-only` transfers style but does not reliably preserve the intended
|
||||
content subject.
|
||||
- `content + style` gives the strongest visible style transfer while keeping
|
||||
the content subject recognizable.
|
||||
- `final-style` reduces source-scene takeover and is the safer fallback.
|
||||
- The seed sweep confirms this tradeoff is stable: `two-card` is consistently
|
||||
visible across seeds, while `final-style` consistently preserves layout
|
||||
better but can be too quiet on some style sources.
|
||||
- `final-style-guarded` is useful when composition leakage remains, but should
|
||||
not be the default.
|
||||
- Heavy layout-away guarding (`stable-style`, away layout `0.2`) is too muted
|
||||
for a general style-transfer method.
|
||||
- Multi-subject content references are a known failure mode: the content card
|
||||
correctly anchors what it sees, so the suite portrait case preserves multiple
|
||||
figures. Crop/clean the content reference before treating this as a style
|
||||
failure.
|
||||
- Highly self-styled content references, especially space/nebula imagery, can
|
||||
resist unrelated graphic styles. In those cases the method stays stable, but
|
||||
the transfer may be subtle unless the default two-card method is used.
|
||||
|
||||
## Lab Command
|
||||
|
||||
Use `style_transfer_stack_test.py` for repeatable checks:
|
||||
|
||||
```bash
|
||||
python docs/recipe-lab/style_transfer_stack_test.py \
|
||||
--method two-card \
|
||||
--content-ref codex-style-variety-20260705/durer_hare.jpg \
|
||||
--style-ref codex-style-variety-20260705/starry_night.jpg \
|
||||
--style-recipe-json docs/recipe-lab/runs/codex-style-transfer-final.json \
|
||||
--prompt "the same hare from the content reference sitting on a simple stone plinth in a clean studio scene, no readable text" \
|
||||
--seed 771201 \
|
||||
--name hare-starry-two-card \
|
||||
--balance
|
||||
```
|
||||
|
||||
The `--style-recipe-json` argument exists because the render server may not
|
||||
have reloaded local Python changes. It injects a lab recipe with the same
|
||||
settings as the V10 `suggest the visual style` override.
|
||||
|
||||
Run the full regression suite:
|
||||
|
||||
```bash
|
||||
python docs/recipe-lab/style_transfer_reliability_suite.py --render --skip-existing
|
||||
```
|
||||
|
||||
Run the seed-stability check:
|
||||
|
||||
```bash
|
||||
python docs/recipe-lab/style_transfer_seed_sweep.py --render --skip-existing
|
||||
```
|
||||
+28
-4
@@ -1,9 +1,10 @@
|
||||
"""V10 tuning tables: the V9 tables plus the V10 additions.
|
||||
"""V10 tuning tables: the V9 tables plus the V10 additions/overrides.
|
||||
|
||||
V10 adds quick recipes for the four roles that V9 left manual-only
|
||||
(palette, environment, framing, loose), the per-card timing table, the
|
||||
stack's balance budgets, and the manual structure/finish layer-dial split.
|
||||
Everything else re-exports V9 so the two versions cannot drift apart.
|
||||
(palette, environment, framing, loose), overrides the visual-style recipe,
|
||||
the per-card timing table, the stack's balance budgets, and the manual
|
||||
structure/finish layer-dial split. Shared tables still re-export V9; V10-only
|
||||
behavior lives in this module so V9 remains frozen.
|
||||
"""
|
||||
|
||||
from ._v9 import v9
|
||||
@@ -18,6 +19,7 @@ STYLE_LAYER_PULL = _v9_recipes.STYLE_LAYER_PULL
|
||||
PALETTE_LAYER_PULL = _v9_recipes.PALETTE_LAYER_PULL
|
||||
MATERIAL_LAYER_PULL = _v9_recipes.MATERIAL_LAYER_PULL
|
||||
LIGHTING_LAYER_PULL = _v9_recipes.LIGHTING_LAYER_PULL
|
||||
STYLE_TRANSFER_LAYER_PULL = [0.062, 0.087, 0.113, 0.15, 0.2, 0.25, 1.25, 3.438, 6.875, 1.375, 5.5, 1.5]
|
||||
|
||||
role_pull_defaults = _v9_recipes.role_pull_defaults
|
||||
role_layer_pull_defaults = _v9_recipes.role_layer_pull_defaults
|
||||
@@ -28,6 +30,28 @@ effective_image_strength = _v9_recipes.effective_image_strength
|
||||
# whisper-defaults philosophy (soft caps, avoid-subject policies).
|
||||
QUICK_RECIPES = dict(_v9_recipes.QUICK_RECIPES)
|
||||
QUICK_RECIPES.update({
|
||||
"style gentle": {
|
||||
"role": "style",
|
||||
"treatment": "strong blur",
|
||||
"color": 1.0,
|
||||
# Render-tuned 2026-07-05 for V10 only: the inherited palette-wash
|
||||
# style recipe had collapsed into a color-grade twin of "suggest the
|
||||
# color palette". Strong blur keeps broad medium/finish cues alive
|
||||
# while muting small details, and the finish-heavy table suppresses the
|
||||
# source's structure layers. Palette-only safety stays with the
|
||||
# separate palette recipe.
|
||||
"detail": 0.3,
|
||||
"study": "384",
|
||||
"framing": "stack",
|
||||
"subject": "avoid",
|
||||
"early": 0.85,
|
||||
"late": 0.85,
|
||||
"guard": False,
|
||||
"cap": 0.65,
|
||||
"shape": 0.85,
|
||||
"global": 1.85,
|
||||
"layers": STYLE_TRANSFER_LAYER_PULL,
|
||||
},
|
||||
"palette only": {
|
||||
"role": "palette",
|
||||
"treatment": "palette wash",
|
||||
|
||||
@@ -277,6 +277,58 @@ class KreaV10CardBehaviorTests(unittest.TestCase):
|
||||
self.assertEqual(card["v9_strength_cap"], 0.9)
|
||||
self.assertEqual(card["resolved_subject_policy"], "avoid")
|
||||
|
||||
def test_visual_style_recipe_uses_v10_style_transfer_override(self):
|
||||
card = self._build(**{
|
||||
"Use image for": "suggest the visual style",
|
||||
"How strongly this image guides": 1.6,
|
||||
})
|
||||
|
||||
self.assertEqual(card["source_version"], "v10")
|
||||
self.assertEqual(card["quick_recipe"], "style gentle")
|
||||
self.assertFalse(card["manual_controls_active"])
|
||||
self.assertFalse(card["custom_recipe"])
|
||||
self.assertEqual(card["resolved_role"], "style")
|
||||
self.assertEqual(card["resolved_treatment"], "strong blur")
|
||||
self.assertAlmostEqual(card["resolved_color_keep"], 1.0)
|
||||
self.assertAlmostEqual(card["resolved_detail"], 0.3)
|
||||
self.assertEqual(card["resolved_reference_resolution"], "384")
|
||||
self.assertEqual(card["resolved_reference_fit"], "stack")
|
||||
self.assertEqual(card["resolved_subject_policy"], "avoid")
|
||||
self.assertAlmostEqual(card["resolved_early_multiplier"], 0.85)
|
||||
self.assertAlmostEqual(card["resolved_late_multiplier"], 0.85)
|
||||
self.assertEqual(card["v9_strength_cap"], 0.65)
|
||||
self.assertAlmostEqual(card["requested_strength"], 1.6)
|
||||
self.assertAlmostEqual(card["strength"], 0.65)
|
||||
self.assertAlmostEqual(card["resolved_shape_pull"], 0.85)
|
||||
self.assertGreater(card["resolved_shape_pull"], 0.4)
|
||||
self.assertAlmostEqual(card["resolved_global_pull"], 1.85)
|
||||
self.assertEqual(card["resolved_layer_pull"], self.nodes.recipes.STYLE_TRANSFER_LAYER_PULL)
|
||||
self.assertEqual(card["resolved_focus"], "")
|
||||
self.assertEqual(card["resolved_direction"], "toward")
|
||||
self.assertEqual(card["resolved_timing"], "recipe")
|
||||
|
||||
def test_visual_style_recipe_keeps_contract_under_v10_direction_and_timing(self):
|
||||
card = self._build(**{
|
||||
"Use image for": "suggest the visual style",
|
||||
"Guide direction": "away from this image",
|
||||
"When this card guides": "final details only",
|
||||
"Structure layers pull": 0.25,
|
||||
"Finish layers pull": 2.0,
|
||||
})
|
||||
|
||||
self.assertEqual(card["resolved_direction"], "away")
|
||||
self.assertEqual(card["direction"], "away")
|
||||
self.assertEqual(card["resolved_timing"], "late")
|
||||
self.assertEqual(card["resolved_subject_policy"], "avoid")
|
||||
self.assertEqual(card["resolved_role"], "style")
|
||||
self.assertEqual(card["resolved_treatment"], "strong blur")
|
||||
self.assertAlmostEqual(card["resolved_early_multiplier"], 0.0)
|
||||
self.assertAlmostEqual(card["resolved_late_multiplier"], 0.85)
|
||||
# V10 layer dials are manual-mode only; built-in recipe layers stay stable.
|
||||
self.assertEqual(card["resolved_layer_pull"], self.nodes.recipes.STYLE_TRANSFER_LAYER_PULL)
|
||||
self.assertAlmostEqual(card["resolved_shape_pull"], 0.85)
|
||||
self.assertAlmostEqual(card["resolved_global_pull"], 1.85)
|
||||
|
||||
def test_away_direction_forces_subject_avoid(self):
|
||||
card = self._build(**{
|
||||
"Use image for": "keep the same subject",
|
||||
@@ -494,6 +546,66 @@ class KreaV10EncoderBehaviorTests(unittest.TestCase):
|
||||
finally:
|
||||
restore_encoder_machinery(cls, original)
|
||||
|
||||
def test_visual_style_recipe_targets_finish_layers_and_balances_predictably(self):
|
||||
cls = self.nodes.KGTextEncodeKreaImageReferencesV10
|
||||
captured = {}
|
||||
original = patch_encoder_machinery(cls, captured)
|
||||
try:
|
||||
style_card = self.nodes.KGKrea2ImageGuideCardV10().build(**{
|
||||
"Reference image": "style-ref",
|
||||
"How strongly this image guides": 0.9,
|
||||
"Use image for": "suggest the visual style",
|
||||
})[0]
|
||||
self._run(
|
||||
captured,
|
||||
**{
|
||||
"Image slider feel": "artist friendly - soft at low values",
|
||||
"When images guide": "guide the whole image",
|
||||
"Balance strong cards": "off - use my values",
|
||||
"Reference 1 guide card": style_card,
|
||||
},
|
||||
)
|
||||
image_entries = [e for e in captured["compose_calls"][0] if e[0] == "image-delta"]
|
||||
self.assertEqual(len(image_entries), 1)
|
||||
_, token_weight, pooled_weight, token_layers = image_entries[0]
|
||||
effective = 0.65 ** 1.6
|
||||
token_target = effective * 0.85
|
||||
|
||||
self.assertAlmostEqual(token_weight, token_target - 1.0)
|
||||
self.assertAlmostEqual(pooled_weight, effective * 1.85 - 1.0)
|
||||
self.assertLess(token_layers[0], 0.0)
|
||||
self.assertLess(token_layers[3], 0.0)
|
||||
self.assertGreater(token_layers[8], 1.5)
|
||||
self.assertGreater(token_layers[10], 1.0)
|
||||
|
||||
captured["compose_calls"] = []
|
||||
self._run(
|
||||
captured,
|
||||
**{
|
||||
"Image slider feel": "literal slider values",
|
||||
"When images guide": "guide the whole image",
|
||||
"Balance strong cards": "strict balance",
|
||||
"Reference 1 guide card": style_card,
|
||||
"Reference 2 guide card": style_card,
|
||||
},
|
||||
)
|
||||
balanced_entries = [
|
||||
e for e in captured["compose_calls"][0] if e[0] == "image-delta"
|
||||
]
|
||||
self.assertEqual(len(balanced_entries), 2)
|
||||
# The recipe caps the literal 0.9 slider to 0.65. Target departures:
|
||||
# token: 0.65 * 0.85 - 1 = -0.4475,
|
||||
# pooled: 0.65 * 1.85 - 1 = 0.2025.
|
||||
# The balancer budgets by the larger axis per card, so this stack
|
||||
# stays below the strict 1.5 budget and should be unchanged.
|
||||
scale = 1.0
|
||||
for _, token_weight, pooled_weight, token_layers in balanced_entries:
|
||||
self.assertAlmostEqual(token_weight, (0.65 * 0.85 - 1.0) * scale)
|
||||
self.assertAlmostEqual(pooled_weight, (0.65 * 1.85 - 1.0) * scale)
|
||||
self.assertGreater(token_layers[8], token_layers[3])
|
||||
finally:
|
||||
restore_encoder_machinery(cls, original)
|
||||
|
||||
|
||||
class KreaV10CustomRecipeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user