Retune V10 style transfer recipe

This commit is contained in:
Kevin Gilper
2026-07-05 19:54:46 -04:00
parent 9ac00b2d88
commit 5447ebdb52
9 changed files with 964 additions and 7 deletions
+4 -2
View File
@@ -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]
+3
View File
@@ -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())
+9 -1
View File
@@ -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
View File
@@ -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",
+112
View File
@@ -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