Krea 2 Realism recipe: t2i/i2i templates + inpaint/i2i/upscale editor-modal toggles (abliterated encoder, realism/bypass LoRAs, ClownsharKSampler in i2i; seam-safe qwen VAE in masked/tiled modes). Plus full-frame i2i editor button, Krea 2 Turbo style-LoRA picker, and viewer/model-settings updates.

This commit is contained in:
chrmc40
2026-06-27 15:31:10 -07:00
parent fd3bf0c675
commit 2924fcd9e2
33 changed files with 5649 additions and 1895 deletions
+1
View File
@@ -8,6 +8,7 @@ data/*
!data/prompts/
!data/models/
!data/templates/
!data/loras/
!data/tag-builder/
data/models/_hash_index.json
cache/
+1
View File
@@ -49,6 +49,7 @@ from .core import tag_api # noqa: F401
from .core import wildcard_api # noqa: F401
from .core import history_api # noqa: F401
from .core import model_api # noqa: F401
from .core import lora_catalog # noqa: F401
from .core import civitai_api # noqa: F401
from .core import system_api # noqa: F401
from .core import node_install_api # noqa: F401
+109
View File
@@ -0,0 +1,109 @@
# LoRA catalog — curated, scope-filtered LoRA packs the model panel offers as
# swappable "styles". Data lives in data/loras/*.json (shipped) and
# user/PromptChain/loras/*.json (user-added, shadows by id). Each file:
# { "scope": {type, architecture, family}, "loras": [ {id,label,filename,
# folder,trigger,strength_default,size_bytes,source}, ... ] }
# Served scope-filtered to the active model so e.g. Krea 2 Turbo styles only
# surface on Krea 2 Turbo.
import json
from pathlib import Path
from typing import Optional
from aiohttp import web
import server
import folder_paths
routes = server.PromptServer.instance.routes
_cache: Optional[list] = None
_cache_signature: Optional[tuple] = None
def _system_dir() -> Path:
return Path(__file__).parent.parent / "data" / "loras"
def _user_dir() -> Path:
return Path(folder_paths.get_user_directory()) / "PromptChain" / "loras"
def _dir_signature() -> tuple:
# Mirror model_settings' mtime watch: a dir's mtime bumps when files are
# added/removed/atomically replaced (a /deploy pull), so shipped catalogs
# refresh without a restart. In-place edits to an existing file don't bump
# the dir mtime — those need a restart.
sig = []
for directory in (_system_dir(), _user_dir()):
try:
sig.append(directory.stat().st_mtime_ns)
except OSError:
sig.append(0)
return tuple(sig)
def _read_json(path: Path) -> Optional[dict]:
try:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
return data if isinstance(data, dict) else None
except Exception:
return None
def _ingest() -> list:
"""All catalog files as (scope, loras) records. User dir is read last so a
user catalog with the same lora id shadows the shipped one."""
by_id: dict[str, dict] = {}
order: list[str] = []
for directory in (_system_dir(), _user_dir()):
if not directory.is_dir():
continue
for path in sorted(directory.glob("*.json")):
data = _read_json(path)
if not data:
continue
scope = data.get("scope", {})
for lora in data.get("loras", []):
lid = lora.get("id") or lora.get("filename")
if not lid:
continue
if lid not in by_id:
order.append(lid)
by_id[lid] = {**lora, "scope": scope}
return [by_id[lid] for lid in order]
def _get_all() -> list:
global _cache, _cache_signature
sig = _dir_signature()
if _cache is not None and sig == _cache_signature:
return _cache
_cache = _ingest()
_cache_signature = sig
return _cache
def _matches_scope(scope: dict, architecture: str, family: Optional[str]) -> bool:
scope_type = scope.get("type", "architecture")
if scope_type == "family":
return scope.get("architecture") == architecture and scope.get("family") == family
return scope.get("architecture") == architecture
def list_loras(architecture: str, family: Optional[str] = None) -> list:
"""Scope-filtered LoRAs for the active model, scope stripped from each."""
out = []
for lora in _get_all():
if _matches_scope(lora.get("scope", {}), architecture, family):
out.append({k: v for k, v in lora.items() if k != "scope"})
return out
@routes.get("/promptchain/loras/catalog")
async def _api_lora_catalog(request):
architecture = request.query.get("architecture", "").strip()
family = request.query.get("family", "").strip() or None
if not architecture:
return web.json_response({"loras": []})
return web.json_response({"loras": list_loras(architecture, family)})
+95
View File
@@ -0,0 +1,95 @@
{
"scope": { "type": "family", "architecture": "krea2", "family": "krea2_turbo" },
"loras": [
{
"id": "krea2-retroanime",
"label": "Retro Anime",
"filename": "krea2_retroanime.safetensors",
"folder": "loras",
"trigger": "Purple retro anime style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_retroanime.safetensors" }
},
{
"id": "krea2-darkbrush",
"label": "Dark Brush",
"filename": "krea2_darkbrush.safetensors",
"folder": "loras",
"trigger": "monochrome ink wash style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_darkbrush.safetensors" }
},
{
"id": "krea2-sunsetblur",
"label": "Sunset Blur",
"filename": "krea2_sunsetblur.safetensors",
"folder": "loras",
"trigger": "ethereal motion blur style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_sunsetblur.safetensors" }
},
{
"id": "krea2-neondrip",
"label": "Neon Drip",
"filename": "krea2_neondrip.safetensors",
"folder": "loras",
"trigger": "Textured abstract style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_neondrip.safetensors" }
},
{
"id": "krea2-vintagetarot",
"label": "Vintage Tarot",
"filename": "krea2_vintagetarot.safetensors",
"folder": "loras",
"trigger": "vintage tarot style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_vintagetarot.safetensors" }
},
{
"id": "krea2-dotmatrix",
"label": "Dot Matrix",
"filename": "krea2_dotmatrix.safetensors",
"folder": "loras",
"trigger": "Monochrome stippling style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_dotmatrix.safetensors" }
},
{
"id": "krea2-softwatercolor",
"label": "Soft Watercolor",
"filename": "krea2_softwatercolor.safetensors",
"folder": "loras",
"trigger": "Art Deco watercolor style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_softwatercolor.safetensors" }
},
{
"id": "krea2-kidsdrawing",
"label": "Kids Drawing",
"filename": "krea2_kidsdrawing.safetensors",
"folder": "loras",
"trigger": "naive expressive sketch style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_kidsdrawing.safetensors" }
},
{
"id": "krea2-rainywindow",
"label": "Rainy Window",
"filename": "krea2_rainywindow.safetensors",
"folder": "loras",
"trigger": "rainy window style",
"strength_default": 1.0,
"size_bytes": 469291992,
"source": { "provider": "huggingface", "repo": "Comfy-Org/Krea-2", "path": "loras/krea2_rainywindow.safetensors" }
}
]
}
+13
View File
@@ -96,6 +96,19 @@
"path": "vae/qwen_image_vae.safetensors"
},
"sha256": null
},
{
"label": "Filter-Bypass Projector-Scale LoRA",
"folder": "loras",
"filename": "krea2_filter_bypass_projector_scale.safetensors",
"size_bytes": 268,
"optional": true,
"source": {
"provider": "huggingface",
"repo": "Beinsezii/Krea-2-Raw-Projector-Scale-LoRA-Diffusers",
"path": "pytorch_lora_weights.safetensors"
},
"sha256": null
}
],
"nodes": {
+8 -8
View File
@@ -38,7 +38,7 @@
116
],
"widgets_values": [
""
"blurry, low-resolution, grainy, pixelated, distorted, compression artifacts, watermark"
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
@@ -57,10 +57,10 @@
"widgets_values": [
826502184750291,
"randomize",
30,
25,
3.5,
"er_sde",
"beta",
"euler",
"simple",
1
],
"properties": {
@@ -145,8 +145,8 @@
144
],
"widgets_values": [
1024,
1024,
1408,
1408,
1
],
"properties": {
@@ -183,8 +183,8 @@
"widgets_values": [
0.90625,
0.5,
1024,
1024
1408,
1408
],
"properties": {
"Node name for S&R": "ModelSamplingFlux"
+316 -288
View File
@@ -1,289 +1,317 @@
{
"name": "Image-to-Image (High Detail)",
"category": "Generation",
"order": 4,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782364000,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9f0a",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
820.0,
20.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1440.0,
120.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
0.6
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEUtils_VAEDecodeTiled",
"offset": [
1800.0,
20.0
],
"size": [
300,
220
],
"widgets_values": [
-1,
false,
512,
64,
4096,
64
],
"properties": {
"Node name for S&R": "VAEUtils_VAEDecodeTiled"
}
},
{
"type": "SaveImage",
"offset": [
2140.0,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/img2img-highdetail/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
820.0,
240.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1100.0,
280.0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_bf16.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "LoadImage",
"offset": [
780.0,
420.0
],
"size": [
270,
340
],
"widgets_values": [
"",
"image"
],
"properties": {
"Node name for S&R": "LoadImage"
}
},
{
"type": "VAEUtils_CustomVAELoader",
"offset": [
1440.0,
620.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"Wan2.1_VAE_upscale2x_imageonly_real_v1.safetensors",
true
],
"properties": {
"Node name for S&R": "VAEUtils_CustomVAELoader"
}
},
{
"type": "ConditioningKrea2Rebalance",
"offset": [
1100.0,
20.0
],
"size": [
300,
106
],
"widgets_values": [
2.0,
"1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.6,2.5,1.05,2.0,1.0"
],
"properties": {
"Node name for S&R": "ConditioningKrea2Rebalance"
}
},
{
"type": "VAEEncode",
"offset": [
1130.0,
500.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"type": "VAELoader",
"offset": [
780.0,
790.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 9,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 0
},
{
"from_node_idx": 10,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 1
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
}
]
{
"name": "Image-to-Image (High Detail)",
"category": "Generation",
"order": 4,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782364000,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9f0a",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
820.0,
20.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1440.0,
120.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
0.6
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEUtils_VAEDecodeTiled",
"offset": [
1800.0,
20.0
],
"size": [
300,
220
],
"widgets_values": [
-1,
false,
512,
64,
4096,
64
],
"properties": {
"Node name for S&R": "VAEUtils_VAEDecodeTiled"
}
},
{
"type": "SaveImage",
"offset": [
2140.0,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/img2img-highdetail/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
820.0,
240.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1100.0,
280.0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_bf16.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "LoadImage",
"offset": [
780.0,
420.0
],
"size": [
270,
340
],
"widgets_values": [
"",
"image"
],
"properties": {
"Node name for S&R": "LoadImage"
}
},
{
"type": "VAEUtils_CustomVAELoader",
"offset": [
1440.0,
620.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"Wan2.1_VAE_upscale2x_imageonly_real_v1.safetensors",
true
],
"properties": {
"Node name for S&R": "VAEUtils_CustomVAELoader"
}
},
{
"type": "ConditioningKrea2Rebalance",
"offset": [
1100.0,
20.0
],
"size": [
300,
106
],
"widgets_values": [
2.0,
"1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.6,2.5,1.05,2.0,1.0"
],
"properties": {
"Node name for S&R": "ConditioningKrea2Rebalance"
}
},
{
"type": "VAEEncode",
"offset": [
1130.0,
500.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"type": "VAELoader",
"offset": [
780.0,
790.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "CLIPTextEncode",
"offset": [
820.0,
220.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 11,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 9,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 0
},
{
"from_node_idx": 10,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 1
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 11,
"to_slot": 0
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
},
{
"from_slot": 2,
"to_node_idx": 11,
"to_slot": 1
}
]
}
+267 -239
View File
@@ -1,240 +1,268 @@
{
"name": "Image-to-Image",
"category": "Generation",
"order": 1,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782327800,
"id": "krea2-turbo-i2i",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1699.9999685974124,
160.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
0.6
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEDecode",
"offset": [
1750.0000429382326,
20.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [
2090.0000285339356,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/img2img/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
860.0000519714356,
200.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1239.9999069433584,
0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_fp8_scaled.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "LoadImage",
"offset": [
1240.0,
210.0
],
"size": [
270,
340
],
"widgets_values": [
"",
"image"
],
"properties": {
"Node name for S&R": "LoadImage"
}
},
{
"type": "VAELoader",
"offset": [
1239.993419293212,
390.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "VAEEncode",
"offset": [
1610.0,
210.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEEncode"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 1
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
}
]
{
"name": "Image-to-Image",
"category": "Generation",
"order": 1,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782327800,
"id": "krea2-turbo-i2i",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1699.9999685974124,
160.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
0.6
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEDecode",
"offset": [
1750.0000429382326,
20.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [
2090.0000285339356,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/img2img/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
860.0000519714356,
200.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1239.9999069433584,
0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_fp8_scaled.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "LoadImage",
"offset": [
1240.0,
210.0
],
"size": [
270,
340
],
"widgets_values": [
"",
"image"
],
"properties": {
"Node name for S&R": "LoadImage"
}
},
{
"type": "VAELoader",
"offset": [
1239.993419293212,
390.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "VAEEncode",
"offset": [
1610.0,
210.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
200.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 9,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 1
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 0
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
},
{
"from_slot": 2,
"to_node_idx": 9,
"to_slot": 1
}
]
}
+140
View File
@@ -0,0 +1,140 @@
{
"name": "Image-to-Image (Realism)",
"category": "Generation",
"order": 3,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782328900,
"id": "krea2-turbo-realism-i2i",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [900, 0],
"size": [225, 116],
"widgets_values": [""],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "CLIPTextEncode",
"offset": [900, 180],
"size": [225, 116],
"widgets_values": [""],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "ClownsharKSampler_Beta",
"offset": [1700, 160],
"size": [373.90625, 756],
"widgets_values": [0.5, "exponential/ddim", "beta57", 8, -1, 0.6, 1, 1033759726676101, "randomize", "standard", false],
"properties": {
"Node name for S&R": "ClownsharKSampler_Beta",
"aux_id": "tashiscool/RES4LYF"
}
},
{
"type": "VAEDecode",
"offset": [2120, 20],
"size": [225, 72],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [2120, 140],
"size": [276.984375, 322.984375],
"widgets_values": ["Krea2-Realism/img2img/%date:yyyy-MM-dd_hhmmss%"],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [540, 340],
"size": [346.78125, 117.328125],
"widgets_values": ["Huihui-Qwen3-VL-4B-Instruct-abliterated-fp8_scaled.safetensors", "krea2", "default"],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [1240, -40],
"size": [371.65625, 82.65625],
"widgets_values": ["krea2_turbo_bf16.safetensors", "default"],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "LoadImage",
"offset": [1240, 420],
"size": [270, 340],
"widgets_values": ["", "image"],
"properties": {
"Node name for S&R": "LoadImage"
}
},
{
"type": "VAELoader",
"offset": [820, 600],
"size": [366.40625, 82.65625],
"widgets_values": ["wan_2.1_vae.safetensors"],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "VAEEncode",
"offset": [1560, 300],
"size": [225, 72],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEEncode"
}
},
{
"type": "LoraLoaderModelOnly",
"offset": [1240, 90],
"size": [371.65625, 82],
"widgets_values": ["krea2_turbo_projector_scale.safetensors", 0.5],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
}
},
{
"type": "LoraLoaderModelOnly",
"offset": [1240, 240],
"size": [371.65625, 82],
"widgets_values": ["Krea2-realism-V1.safetensors", 0.6],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
}
}
],
"connections": [
{"from_node_idx": 0, "from_slot": 0, "to_node_idx": 2, "to_slot": 1},
{"from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 2},
{"from_node_idx": 2, "from_slot": 0, "to_node_idx": 3, "to_slot": 0},
{"from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 0},
{"from_node_idx": 5, "from_slot": 0, "to_node_idx": 0, "to_slot": 0},
{"from_node_idx": 5, "from_slot": 0, "to_node_idx": 1, "to_slot": 0},
{"from_node_idx": 6, "from_slot": 0, "to_node_idx": 10, "to_slot": 0},
{"from_node_idx": 10, "from_slot": 0, "to_node_idx": 11, "to_slot": 0},
{"from_node_idx": 11, "from_slot": 0, "to_node_idx": 2, "to_slot": 0},
{"from_node_idx": 9, "from_slot": 0, "to_node_idx": 2, "to_slot": 3},
{"from_node_idx": 7, "from_slot": 0, "to_node_idx": 9, "to_slot": 0},
{"from_node_idx": 8, "from_slot": 0, "to_node_idx": 9, "to_slot": 1},
{"from_node_idx": 8, "from_slot": 0, "to_node_idx": 3, "to_slot": 1}
],
"anchorConnections": [
{"from_slot": 1, "to_node_idx": 0, "to_slot": 1},
{"from_slot": 2, "to_node_idx": 1, "to_slot": 1}
]
}
+129
View File
@@ -0,0 +1,129 @@
{
"name": "Text-to-Image (Realism)",
"category": "Generation",
"order": 2,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782328900,
"id": "d8f3a1c2-6e4b-4f9a-b1c7-3a8e5d2f9b14",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [900, 0],
"size": [225, 116],
"widgets_values": [""],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "CLIPTextEncode",
"offset": [900, 180],
"size": [225, 116],
"widgets_values": [""],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "ClownsharKSampler_Beta",
"offset": [1700, 160],
"size": [373.90625, 756],
"widgets_values": [0.5, "exponential/ddim", "beta57", 8, -1, 1, 1, 1033759726676101, "randomize", "standard", false],
"properties": {
"Node name for S&R": "ClownsharKSampler_Beta",
"aux_id": "tashiscool/RES4LYF"
}
},
{
"type": "VAEDecode",
"offset": [2120, 20],
"size": [225, 72],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [2120, 140],
"size": [276.984375, 322.984375],
"widgets_values": ["Krea2-Realism/%date:yyyy-MM-dd_hhmmss%"],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [540, 340],
"size": [346.78125, 117.328125],
"widgets_values": ["Huihui-Qwen3-VL-4B-Instruct-abliterated-fp8_scaled.safetensors", "krea2", "default"],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [1240, -40],
"size": [371.65625, 82.65625],
"widgets_values": ["krea2_turbo_bf16.safetensors", "default"],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "EmptySD3LatentImage",
"offset": [1240, 420],
"size": [324, 144],
"widgets_values": [1152, 2048, 1],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
}
},
{
"type": "VAELoader",
"offset": [1240, 600],
"size": [366.40625, 82.65625],
"widgets_values": ["wan_2.1_vae.safetensors"],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "LoraLoaderModelOnly",
"offset": [1240, 90],
"size": [371.65625, 82],
"widgets_values": ["krea2_turbo_projector_scale.safetensors", 0.5],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
}
},
{
"type": "LoraLoaderModelOnly",
"offset": [1240, 240],
"size": [371.65625, 82],
"widgets_values": ["Krea2-realism-V1.safetensors", 0.6],
"properties": {
"Node name for S&R": "LoraLoaderModelOnly"
}
}
],
"connections": [
{"from_node_idx": 0, "from_slot": 0, "to_node_idx": 2, "to_slot": 1},
{"from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 2},
{"from_node_idx": 2, "from_slot": 0, "to_node_idx": 3, "to_slot": 0},
{"from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 0},
{"from_node_idx": 5, "from_slot": 0, "to_node_idx": 0, "to_slot": 0},
{"from_node_idx": 5, "from_slot": 0, "to_node_idx": 1, "to_slot": 0},
{"from_node_idx": 6, "from_slot": 0, "to_node_idx": 9, "to_slot": 0},
{"from_node_idx": 9, "from_slot": 0, "to_node_idx": 10, "to_slot": 0},
{"from_node_idx": 10, "from_slot": 0, "to_node_idx": 2, "to_slot": 0},
{"from_node_idx": 7, "from_slot": 0, "to_node_idx": 2, "to_slot": 3},
{"from_node_idx": 8, "from_slot": 0, "to_node_idx": 3, "to_slot": 1}
],
"anchorConnections": [
{"from_slot": 1, "to_node_idx": 0, "to_slot": 1},
{"from_slot": 2, "to_node_idx": 1, "to_slot": 1}
]
}
+273 -245
View File
@@ -1,246 +1,274 @@
{
"name": "Text-to-Image (High Detail)",
"category": "Generation",
"order": 3,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782362000,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9f09",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
820.0,
20.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1440.0,
120.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
1
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEUtils_VAEDecodeTiled",
"offset": [
1800.0,
20.0
],
"size": [
300,
220
],
"widgets_values": [
-1,
false,
512,
64,
4096,
64
],
"properties": {
"Node name for S&R": "VAEUtils_VAEDecodeTiled"
}
},
{
"type": "SaveImage",
"offset": [
2140.0,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/highdetail/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
820.0,
240.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1100.0,
280.0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_bf16.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "EmptySD3LatentImage",
"offset": [
1100.0,
420.0
],
"size": [
324,
144
],
"widgets_values": [
1024,
1536,
1
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
}
},
{
"type": "VAEUtils_CustomVAELoader",
"offset": [
1440.0,
620.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"Wan2.1_VAE_upscale2x_imageonly_real_v1.safetensors",
true
],
"properties": {
"Node name for S&R": "VAEUtils_CustomVAELoader"
}
},
{
"type": "ConditioningKrea2Rebalance",
"offset": [
1100.0,
20.0
],
"size": [
300,
106
],
"widgets_values": [
2.0,
"1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.6,2.5,1.05,2.0,1.0"
],
"properties": {
"Node name for S&R": "ConditioningKrea2Rebalance"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
}
]
{
"name": "Text-to-Image (High Detail)",
"category": "Generation",
"order": 3,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782362000,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9f09",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
820.0,
20.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1440.0,
120.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
1
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEUtils_VAEDecodeTiled",
"offset": [
1800.0,
20.0
],
"size": [
300,
220
],
"widgets_values": [
-1,
false,
512,
64,
4096,
64
],
"properties": {
"Node name for S&R": "VAEUtils_VAEDecodeTiled"
}
},
{
"type": "SaveImage",
"offset": [
2140.0,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/highdetail/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
820.0,
240.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1100.0,
280.0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_bf16.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "EmptySD3LatentImage",
"offset": [
1100.0,
420.0
],
"size": [
324,
144
],
"widgets_values": [
1024,
1536,
1
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
}
},
{
"type": "VAEUtils_CustomVAELoader",
"offset": [
1440.0,
620.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"Wan2.1_VAE_upscale2x_imageonly_real_v1.safetensors",
true
],
"properties": {
"Node name for S&R": "VAEUtils_CustomVAELoader"
}
},
{
"type": "ConditioningKrea2Rebalance",
"offset": [
1100.0,
20.0
],
"size": [
300,
106
],
"widgets_values": [
2.0,
"1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.6,2.5,1.05,2.0,1.0"
],
"properties": {
"Node name for S&R": "ConditioningKrea2Rebalance"
}
},
{
"type": "CLIPTextEncode",
"offset": [
820.0,
220.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 9,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 9,
"to_slot": 0
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
},
{
"from_slot": 2,
"to_node_idx": 9,
"to_slot": 1
}
]
}
+241 -213
View File
@@ -1,214 +1,242 @@
{
"name": "Text-to-Image",
"category": "Generation",
"order": 0,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782327800,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9e07",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1699.9999685974124,
160.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
1
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEDecode",
"offset": [
1750.0000429382326,
20.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [
2090.0000285339356,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
860.0000519714356,
200.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1239.9999069433584,
0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_fp8_scaled.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "EmptySD3LatentImage",
"offset": [
1249.9999315600576,
200.0
],
"size": [
324,
144
],
"widgets_values": [
1024,
1536,
1
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
}
},
{
"type": "VAELoader",
"offset": [
1239.993419293212,
390.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
}
]
{
"name": "Text-to-Image",
"category": "Generation",
"order": 0,
"scope": {
"type": "family",
"architecture": "krea2",
"family": "krea2_turbo"
},
"created_at": 1782327800,
"id": "b7c3e8a1-4f2d-4a6b-9c0e-2a1f6d3b9e07",
"nodes": [
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
},
{
"type": "KSampler",
"offset": [
1699.9999685974124,
160.0
],
"size": [
330,
474
],
"widgets_values": [
826502184750291,
"randomize",
8,
1,
"euler",
"simple",
1
],
"properties": {
"Node name for S&R": "KSampler"
}
},
{
"type": "VAEDecode",
"offset": [
1750.0000429382326,
20.0
],
"size": [
225,
72
],
"widgets_values": [],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"type": "SaveImage",
"offset": [
2090.0000285339356,
-10.0
],
"size": [
276.984375,
322.984375
],
"widgets_values": [
"Krea2/%date:yyyy-MM-dd_hhmmss%"
],
"properties": {}
},
{
"type": "CLIPLoader",
"offset": [
860.0000519714356,
200.0
],
"size": [
346.78125,
117.328125
],
"widgets_values": [
"qwen3vl_4b_fp8_scaled.safetensors",
"krea2",
"default"
],
"properties": {
"Node name for S&R": "CLIPLoader"
}
},
{
"type": "UNETLoader",
"offset": [
1239.9999069433584,
0
],
"size": [
371.65625,
82.65625
],
"widgets_values": [
"krea2_turbo_fp8_scaled.safetensors",
"default"
],
"properties": {
"Node name for S&R": "UNETLoader"
}
},
{
"type": "EmptySD3LatentImage",
"offset": [
1249.9999315600576,
200.0
],
"size": [
324,
144
],
"widgets_values": [
1024,
1536,
1
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
}
},
{
"type": "VAELoader",
"offset": [
1239.993419293212,
390.0
],
"size": [
366.40625,
82.65625
],
"widgets_values": [
"qwen_image_vae.safetensors"
],
"properties": {
"Node name for S&R": "VAELoader"
}
},
{
"type": "CLIPTextEncode",
"offset": [
899.9999977111815,
200.0
],
"size": [
225,
116
],
"widgets_values": [
""
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
}
}
],
"connections": [
{
"from_node_idx": 0,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 1
},
{
"from_node_idx": 8,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 2
},
{
"from_node_idx": 1,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 0
},
{
"from_node_idx": 2,
"from_slot": 0,
"to_node_idx": 3,
"to_slot": 0
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 0,
"to_slot": 0
},
{
"from_node_idx": 5,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 0
},
{
"from_node_idx": 6,
"from_slot": 0,
"to_node_idx": 1,
"to_slot": 3
},
{
"from_node_idx": 7,
"from_slot": 0,
"to_node_idx": 2,
"to_slot": 1
},
{
"from_node_idx": 4,
"from_slot": 0,
"to_node_idx": 8,
"to_slot": 0
}
],
"anchorConnections": [
{
"from_slot": 1,
"to_node_idx": 0,
"to_slot": 1
},
{
"from_slot": 2,
"to_node_idx": 8,
"to_slot": 1
}
]
}
+1 -1
View File
@@ -37,7 +37,7 @@ function closeMenu() {
// ── graph traversal ──────────────────────────────────────────────
const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale", "PromptChain_IdeogramSampler"]);
const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale", "PromptChain_IdeogramSampler", "ClownsharKSampler_Beta"]);
const LOAD_IMAGE_TYPES = new Set(["LoadImage", "LoadImageOutput"]);
function _findDownstream(startNode, targetTypes, graph) {
const visited = new Set();
+1 -1
View File
@@ -55,7 +55,7 @@ let focusedGroup = null;
// PromptChain feeding them. Mirrored from main.js — the canonical set.
const KSAMPLER_TYPES = new Set([
"KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale",
"PromptChain_IdeogramSampler",
"PromptChain_IdeogramSampler", "ClownsharKSampler_Beta",
]);
// Workflow-level fullscreen state stash. The preview panels are
+557
View File
@@ -0,0 +1,557 @@
// Editor "i2i": full-frame image-to-image. The Edit modal hands its flattened
// composite (already snapped to a /16 canvas by openI2iHandoff, so the VAE's
// own center-crop is a no-op and the result composites back aligned) to a clean
// LoadImage → VAEEncode → KSampler(denoise) → VAEDecode → PreviewImage
// graph — no mask, no tiling, no crop. The render runs off-screen (PreviewImage
// → temp dir, the user's tabs untouched) and returns to Edit as a new layer.
//
// This is the no-mask sibling of the inpaint subsystem: it reuses prepareInpaint
// (wrapped) for caps + the engine list, the same content-addressed upload, the
// same loader clusters per engine, and the same background-tracker lifecycle.
// The one net-new piece is the clean graph TAIL — the inpaint builders all end
// in PromptChain_MaskedDetail (crop-and-detail), which would downscale a full
// frame to its max_size; i2i samples the whole frame instead.
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { externallyTrackedPrompts, armExternalQueue, disarmExternalQueue } from "./history.js";
import { prepareInpaint, sourcePromptText, createTracker, freshWorkflowId } from "./inpaint-background.js";
import { offscreenIdBase } from "./offscreen-graph.js";
function toast(severity, detail) {
app.extensionManager?.toast?.add({ severity, summary: "i2i", detail, life: 7000 });
}
const numOr = (v, fallback) => (typeof v === "number" && Number.isFinite(v) ? v : fallback);
// Per-engine i2i recipe (denoise/steps/cfg/sampler/scheduler), read off the live
// data/templates/*-i2i.json this session. The modal seeds its dials from this;
// the builders only fall back to it when an option is missing. Keyed by an
// architecture family; krea2/zimage split raw-vs-turbo by filename.
export const I2I_RECIPE_DEFAULTS = {
sdxl: { denoise: 0.55, steps: 35, cfg: 4.0, sampler: "dpmpp_2m_sde", scheduler: "karras" },
flux1: { denoise: 0.55, steps: 25, cfg: 1.0, sampler: "euler", scheduler: "simple", guidance: 3.5 },
flux2: { denoise: 0.55, steps: 25, cfg: 1.0, sampler: "euler", scheduler: "simple", guidance: 3.5 },
krea2_raw: { denoise: 0.35, steps: 40, cfg: 3.5, sampler: "er_sde", scheduler: "beta" },
krea2_turbo: { denoise: 0.35, steps: 8, cfg: 1.0, sampler: "euler", scheduler: "simple" },
zimage_base: { denoise: 0.55, steps: 50, cfg: 4.0, sampler: "dpmpp_sde", scheduler: "beta" },
zimage_turbo: { denoise: 0.55, steps: 9, cfg: 1.3, sampler: "res_multistep", scheduler: "beta" },
generic: { denoise: 0.50, steps: 30, cfg: 4.0, sampler: "euler", scheduler: "normal" },
};
// The single source of truth for i2i defaults: resolve the recipe for an
// architecture/filename, layering the model's own saved gen recipe where it's
// meaningful. Returns a FRESH object (never a shared I2I_RECIPE_DEFAULTS ref).
// `gen` = the model config's t2i KSampler settings (from fetchEngineModels).
// SDXL honors its stored steps/cfg/sampler/scheduler; distilled DiTs
// (flux/krea2/zimage) have arch-fixed recipes a stored t2i gen shouldn't
// override, except flux's step count. The modal reads this via caps — it has no
// table of its own (no drift).
export function i2iRecipe(architecture, filename = "", gen = null) {
const a = String(architecture || "").toLowerCase();
const turbo = /turbo/i.test(filename);
const raw = /raw/i.test(filename) && !turbo;
let base;
if (a === "krea2") base = raw ? I2I_RECIPE_DEFAULTS.krea2_raw : I2I_RECIPE_DEFAULTS.krea2_turbo;
else if (a === "sdxl") base = I2I_RECIPE_DEFAULTS.sdxl;
else if (a === "flux" || a === "flux1") base = I2I_RECIPE_DEFAULTS.flux1;
else if (a === "flux2") base = I2I_RECIPE_DEFAULTS.flux2;
else if (a === "zimage" || a === "z-image") base = turbo ? I2I_RECIPE_DEFAULTS.zimage_turbo : I2I_RECIPE_DEFAULTS.zimage_base;
else base = I2I_RECIPE_DEFAULTS.generic;
const r = { ...base };
if (gen) {
if (a === "sdxl") {
if (Number.isFinite(gen.steps)) r.steps = gen.steps;
if (Number.isFinite(gen.cfg)) r.cfg = gen.cfg;
if (gen.sampler) r.sampler = gen.sampler;
if (gen.scheduler) r.scheduler = gen.scheduler;
} else if ((a === "flux" || a === "flux1") && Number.isFinite(gen.steps)) {
r.steps = gen.steps;
}
}
return r;
}
// Caps for the i2i modal: the inpaint caps minus everything mask-spatial. Full
// frame has no region to resolve, so regional/depth modes and the conditions
// panel (which patch the UNET around a mask) don't apply.
export async function prepareI2I(entry) {
const prepared = await prepareInpaint(entry);
if (!prepared) return null;
prepared.caps.regionalAvailable = false;
prepared.caps.depthAvailable = false;
// P0 engine roster: Source + SDXL / FLUX.1 / Krea 2 (true denoise i2i). Qwen
// Edit is an instruction edit at denoise 1.0 — a different feature, deferred.
// Stamp each engine with its resolved i2i recipe (the modal reads .i2i — no
// recipe table lives in the component).
prepared.caps.engineModels = (prepared.caps.engineModels || [])
.filter((m) => m.architecture !== "qwen_edit")
.map((m) => ({ ...m, i2i: i2iRecipe(m.architecture, m.filename, m.gen) }));
// The Source engine's recipe (only its denoise is used — steps/cfg/sampler are
// inherited from the image's own embedded render). Drives the default denoise,
// so a krea2 source opens at krea2's i2i denoise.
prepared.caps.sourceI2iRecipe = i2iRecipe(prepared.caps.sourceModelInfo?.architecture || "", "", null);
prepared.caps.defaultDenoise = prepared.caps.sourceI2iRecipe.denoise ?? 0.55;
return prepared;
}
// ── graph builders ────────────────────────────────────────────────────────
function probeCombo(type, widget) {
try {
const node = window.LiteGraph?.createNode?.(type);
return node?.widgets?.find((w) => w.name === widget)?.options?.values || [];
} catch { return []; }
}
const PC_NODE = () => ({
prompt: "", mode: "combine", switch_index: 1,
locked: false, disabled: false, cached_output: "", cached_neg_output: "",
iterate_index: 0, iterate_cycle: 1, collapsed: false,
wildcard_modes: "", cached_regions: "",
});
function makeAdder(graph) {
// Render-node ids start far above every live-canvas id so ComfyUI's id-keyed
// output painter never lands this background render on a visible node (see
// offscreen-graph.js). Mirrors the inpaint builders.
let nextId = offscreenIdBase();
return (class_type, inputs) => {
const id = String(nextId++);
graph[id] = { class_type, inputs };
return id;
};
}
// Shared clean tail: encode the loaded image, partially denoise it, decode,
// preview. `modelRef`/`clipRef`/`vaeRef` are [id, slot] links into `graph`.
function attachI2ITail(graph, add, { inputRef, modelRef, clipRef, vaeRef, options, guidance, steps, cfg, sampler, scheduler, clown }) {
const loadId = add("LoadImage", { image: inputRef });
const encId = add("VAEEncode", { pixels: [loadId, 0], vae: vaeRef });
const pc = PC_NODE(); pc.prompt = options.prompt || "";
const pcId = add("PromptChain_PromptChain", pc);
let posRef = [add("CLIPTextEncode", { text: [pcId, 1], clip: clipRef }), 0];
const negRef = [add("CLIPTextEncode", { text: [pcId, 2], clip: clipRef }), 0];
if (typeof guidance === "number") {
posRef = [add("FluxGuidance", { conditioning: posRef, guidance }), 0];
}
const denoise = typeof options.denoise === "number" ? options.denoise : 0.55;
// Realism full-frame i2i: the same RES4LYF ClownsharKSampler_Beta the realism
// t2i template uses. Full-frame output (not a composited patch), so a sampler
// NODE is allowed here. cfg pinned 1 like Turbo; denoise from the dial.
const ksId = clown
? add("ClownsharKSampler_Beta", {
model: modelRef, positive: posRef, negative: negRef, latent_image: [encId, 0],
eta: 0.5, sampler_name: "exponential/ddim", scheduler: "beta57",
steps: steps ?? 8, steps_to_run: -1, denoise, cfg: cfg ?? 1.0,
seed: Math.floor(Math.random() * 0xffffffffff), sampler_mode: "standard", bongmath: false,
})
: add("KSampler", {
model: modelRef, positive: posRef, negative: negRef, latent_image: [encId, 0],
seed: Math.floor(Math.random() * 0xffffffffff),
steps, cfg, sampler_name: sampler, scheduler, denoise,
});
const decId = add("VAEDecode", { samples: [ksId, 0], vae: vaeRef });
const previewId = add("PreviewImage", { images: [decId, 0] });
return { ksId, previewId };
}
// SDXL picker engine: a fresh checkpoint i2i — no embedded render data needed,
// so plain photos / imported images work too. Mirrors buildInpaintEnginePrompt
// upstream (checkpoint + two encodes), clean tail.
export function buildI2IEngineSdxl(data, options) {
const filename = options.engineModel?.filename;
if (!filename) throw new Error("no engine model picked");
const graph = {};
const add = makeAdder(graph);
const ckptId = add("CheckpointLoaderSimple", { ckpt_name: filename });
const gen = options.engineGen || {};
const { ksId } = attachI2ITail(graph, add, {
inputRef: data.input_ref, modelRef: [ckptId, 0], clipRef: [ckptId, 1], vaeRef: [ckptId, 2], options,
steps: numOr(options.steps, numOr(gen.steps, 35)),
cfg: numOr(options.cfg, numOr(gen.cfg, 4)),
sampler: options.sampler || gen.sampler || "dpmpp_2m_sde",
scheduler: options.scheduler || gen.scheduler || "karras",
});
return { graph, detailId: ksId };
}
// FLUX.1 picker engine: guidance rides FluxGuidance on the positive, KSampler
// cfg pinned 1.0 (a real cfg would double sampling). Mirrors buildFlux1InpaintPrompt.
export function buildI2IEngineFlux1(data, options) {
const filename = options.engineModel?.filename;
if (!filename) throw new Error("no engine model picked");
const clipOpts = probeCombo("DualCLIPLoader", "clip_name1");
const clipL = clipOpts.find((o) => /(^|[\\/])clip_l\./i.test(o)) || clipOpts.find((o) => /clip_l/i.test(o));
const t5 = clipOpts.find((o) => /t5xxl.*fp16/i.test(o)) || clipOpts.find((o) => /t5xxl/i.test(o));
const vaeFile = probeCombo("VAELoader", "vae_name").find((o) => /(^|[\\/])ae\.(safetensors|sft)$/i.test(o));
if (!clipL || !t5 || !vaeFile) throw new Error("the FLUX text encoders / VAE aren't installed");
const graph = {};
const add = makeAdder(graph);
const unetId = add("UNETLoader", { unet_name: filename, weight_dtype: "default" });
const clipId = add("DualCLIPLoader", { clip_name1: clipL, clip_name2: t5, type: "flux", device: "default" });
const vaeId = add("VAELoader", { vae_name: vaeFile });
const gen = options.engineGen || {};
const { ksId } = attachI2ITail(graph, add, {
inputRef: data.input_ref, modelRef: [unetId, 0], clipRef: [clipId, 0], vaeRef: [vaeId, 0], options,
guidance: I2I_RECIPE_DEFAULTS.flux1.guidance,
steps: numOr(options.steps, numOr(gen.steps, 25)),
cfg: 1.0,
sampler: options.sampler || gen.sampler || "euler",
scheduler: options.scheduler || gen.scheduler || "simple",
});
return { graph, detailId: ksId };
}
// Krea 2 picker engine: a DiT that wires like FLUX.1 minus the guidance regime
// (cfg-native, no FluxGuidance). RAW needs the resolution shift + full steps +
// real cfg; Turbo is the pinned 8-step/cfg-1 recipe. Mirrors buildKrea2InpaintPrompt.
export function buildI2IEngineKrea2(data, options) {
const filename = options.engineModel?.filename;
if (!filename) throw new Error("no engine model picked");
// Realism mode (krea2_turbo): abliterated Qwen3-VL encoder + wan_2.1 VAE +
// projector/realism LoRAs + the ClownsharKSampler tail. i2i is full-frame (a
// whole new layer, not a composited patch), so wan VAE is safe and the Clown
// sampler node can run — matching the realism t2i template exactly.
const realism = !!options.realism;
const clipOpts = probeCombo("CLIPLoader", "clip_name");
const loraOpts = probeCombo("LoraLoaderModelOnly", "lora_name");
const clipFile = realism
? clipOpts.find((o) => /huihui.*qwen3.?vl.*4b.*abliterated|qwen3.?vl.*4b.*abliterated/i.test(o))
: (clipOpts.find((o) => /qwen3vl.*4b/i.test(o)) || clipOpts.find((o) => /qwen3.?vl.*4b/i.test(o)));
const vaeFile = realism
? probeCombo("VAELoader", "vae_name").find((o) => /wan.*2[._]?1.*vae/i.test(o) && !/upscale|2x/i.test(o))
: probeCombo("VAELoader", "vae_name").find((o) => /qwen.*image.*vae/i.test(o));
const projLora = realism && loraOpts.find((o) => /krea2_turbo_projector_scale/i.test(o));
const realLora = realism && loraOpts.find((o) => /krea2-realism/i.test(o));
if (!clipFile || !vaeFile) throw new Error(realism
? "Realism mode needs the abliterated Qwen3-VL encoder + wan_2.1 VAE installed"
: "the Krea 2 text encoder / VAE aren't installed");
if (realism && (!projLora || !realLora)) throw new Error("Realism mode needs the Krea2-realism-V1 + krea2_turbo_projector_scale LoRAs installed");
const graph = {};
const add = makeAdder(graph);
const unetId = add("UNETLoader", { unet_name: filename, weight_dtype: "default" });
const clipId = add("CLIPLoader", { clip_name: clipFile, type: "krea2", device: "default" });
const vaeId = add("VAELoader", { vae_name: vaeFile });
const raw = /raw/i.test(filename) && !/turbo/i.test(filename);
let modelRef = [unetId, 0];
if (raw) {
// RAW over-shifts on the baked 1.15 mu — replace it with a resolution-aware
// shift (the same node the raw t2i/i2i templates insert). i2i has no
// MaskedDetail crop, so the sampled latent IS the input size — feed those
// real dims, not a stale 1024², or the mu schedule is wrong for non-square /
// non-1024 inputs.
const msfId = add("ModelSamplingFlux", {
model: [unetId, 0], max_shift: 0.90625, base_shift: 0.5,
width: data.width || 1024, height: data.height || 1024,
});
modelRef = [msfId, 0];
}
if (realism) {
const proj = add("LoraLoaderModelOnly", { model: modelRef, lora_name: projLora, strength_model: 0.5 });
const real = add("LoraLoaderModelOnly", { model: [proj, 0], lora_name: realLora, strength_model: 0.6 });
modelRef = [real, 0];
}
const { ksId } = attachI2ITail(graph, add, {
inputRef: data.input_ref, modelRef, clipRef: [clipId, 0], vaeRef: [vaeId, 0], options,
clown: realism,
steps: numOr(options.steps, raw ? 40 : 8),
cfg: numOr(options.cfg, raw ? 3.5 : 1.0),
sampler: options.sampler || (raw ? "er_sde" : "euler"),
scheduler: options.scheduler || (raw ? "beta" : "simple"),
});
return { graph, detailId: ksId };
}
// Architecture of the image's own render graph — picks the conditioning regime
// for the source graft (which model stream / guidance the fresh encodes feed).
// Mirrors inpaint-background's inpaintArch.
function sourceArch(entries) {
const has = (...cls) => entries.some((n) => cls.includes(n?.class_type));
if (has("CheckpointLoaderSimple", "CheckpointLoader")) return "sdxl";
const clipType = String(entries.find((n) => n?.class_type === "CLIPLoader")?.inputs?.type || "").toLowerCase();
if (clipType === "flux2") return "flux2";
if (clipType === "krea2") return "krea2";
if (clipType === "lumina2") return "zimage";
if (has("DualCLIPLoader") || clipType === "flux" || has("UNETLoader", "UnetLoaderGGUF")) return "flux1";
return "other";
}
// Source graft: re-diffuse the image with ITS OWN model. Traces the model / clip
// / vae out of the embedded API prompt (the truest "this image, more diffused")
// and attaches the clean i2i tail, applying the arch's conditioning regime.
// Mirrors buildInpaintPrompt's tracing; swaps the MaskedDetail tail for the
// plain encode/sample/decode. Throws (engine picker is the fallback) if the
// graph can't be traced.
export function buildI2ISource(apiPrompt, data, options) {
const graph = {};
for (const [id, entry] of Object.entries(apiPrompt || {})) {
if (!entry || typeof entry.class_type !== "string") continue;
graph[id] = { class_type: entry.class_type, inputs: JSON.parse(JSON.stringify(entry.inputs || {})) };
}
const idByClass = (...cls) => Object.keys(graph).find((id) => cls.includes(graph[id].class_type));
const isLink = (v) => Array.isArray(v) && v.length === 2 && !!graph[String(v[0])];
const SAMPLER_CLASSES = ["KSampler", "KSamplerAdvanced", "PromptChain_MaskedDetail", "UltimateSDUpscale"];
const anchorId = Object.keys(graph).find((id) => SAMPLER_CLASSES.includes(graph[id].class_type));
if (!anchorId) throw new Error("no sampler in this image's render data");
const src = graph[anchorId];
// Trace to the unpatched model (keep LoRA loaders / ModelSampling shims; drop
// only the AttentionCouple, which bakes the source gen's regional prompts).
let modelRef = src.inputs.model;
while (isLink(modelRef) && graph[String(modelRef[0])].class_type === "PromptChain_AttentionCouple") {
modelRef = graph[String(modelRef[0])].inputs.model;
}
if (!isLink(modelRef)) throw new Error("the sampler has no model wired");
const ckptId = idByClass("CheckpointLoaderSimple", "CheckpointLoader");
const encNodeId = idByClass("CLIPTextEncode");
const regionalId = idByClass("PromptChain_AttentionCouple", "PromptChain_RegionalConditioning");
const clipLoaderId = idByClass("CLIPLoader");
const clipRef = (encNodeId && isLink(graph[encNodeId].inputs.clip) && graph[encNodeId].inputs.clip)
|| (regionalId && isLink(graph[regionalId].inputs.clip) && graph[regionalId].inputs.clip)
|| (clipLoaderId && [clipLoaderId, 0])
|| (ckptId && [ckptId, 1]);
const decId = idByClass("VAEDecode", "VAEUtils_VAEDecodeTiled", "VAEDecodeTiled");
let vaeRef = (decId && isLink(graph[decId].inputs.vae) && graph[decId].inputs.vae)
|| (isLink(src.inputs.vae) && src.inputs.vae)
|| (ckptId && [ckptId, 2]);
if (!clipRef) throw new Error("couldn't trace the clip/vae lines");
const add = makeAdder(graph);
// A decode-only / 2x custom VAE loader can't ENCODE — i2i needs both. Graft a
// stock VAELoader on the shared latent if the trace failed or hit that loader.
const tracedVae = isLink(vaeRef) ? graph[String(vaeRef[0])] : null;
if (!vaeRef || (tracedVae && tracedVae.class_type === "VAEUtils_CustomVAELoader")) {
const vaeName = probeCombo("VAELoader", "vae_name").find((o) => /qwen.*image.*vae/i.test(o))
|| probeCombo("VAELoader", "vae_name").find((o) => /wan.*2[._]?1.*vae/i.test(o) && !/upscale|2x/i.test(o));
if (!vaeName) throw new Error("couldn't trace the clip/vae lines");
vaeRef = [add("VAELoader", { vae_name: vaeName }), 0];
}
const arch = sourceArch(Object.values(graph));
const fluxLike = arch === "flux1" || arch === "flux2";
const { ksId, previewId } = attachI2ITail(graph, add, {
inputRef: data.input_ref, modelRef, clipRef, vaeRef, options,
guidance: fluxLike ? I2I_RECIPE_DEFAULTS.flux1.guidance : undefined,
steps: numOr(options.steps, numOr(src.inputs.steps, 30)),
// flux pins cfg 1.0 (guidance carries it); other arches inherit the dial.
cfg: fluxLike ? 1.0 : numOr(options.cfg, numOr(src.inputs.cfg, 5)),
sampler: options.sampler || (typeof src.inputs.sampler_name === "string" ? src.inputs.sampler_name : "euler"),
scheduler: options.scheduler || (typeof src.inputs.scheduler === "string" ? src.inputs.scheduler : "normal"),
});
// Prune to the new PreviewImage's upstream closure — the source gen (its
// sampler, pose, couple, old encodes, saves, and ITS own PreviewImage/saves)
// all falls away. Walk from the terminal preview so VAEDecode/KSampler/loaders
// are all retained.
const keep = new Set();
const stack = [previewId];
while (stack.length) {
const id = stack.pop();
if (!id || keep.has(id)) continue;
keep.add(id);
for (const v of Object.values(graph[id].inputs || {})) {
if (isLink(v)) stack.push(String(v[0]));
}
}
for (const id of Object.keys(graph)) if (!keep.has(id)) delete graph[id];
return { graph, detailId: ksId };
}
function buildI2IGraph(data, options) {
if (options.engine === "sdxl-ckpt" && options.engineModel) return buildI2IEngineSdxl(data, options);
if (options.engine === "flux1-unet" && options.engineModel) return buildI2IEngineFlux1(data, options);
if (options.engine === "krea2-unet" && options.engineModel) return buildI2IEngineKrea2(data, options);
return buildI2ISource(data.prompt, data, options);
}
function collectI2IMeta(graph, detailId, data, options) {
const byClass = (...cls) => Object.values(graph).find((n) => cls.includes(n.class_type));
const loader = byClass("CheckpointLoaderSimple", "CheckpointLoader") || byClass("UNETLoader");
const lit = (v) => (Array.isArray(v) || v === undefined ? null : v);
const ks = graph[detailId].inputs;
return {
parent_filename: data.input_ref,
model: lit(loader?.inputs?.ckpt_name) || lit(loader?.inputs?.unet_name) || null,
seed: lit(ks.seed), steps: lit(ks.steps), cfg: lit(ks.cfg),
sampler: lit(ks.sampler_name), scheduler: lit(ks.scheduler),
denoise: options.denoise ?? null, prompt: options.prompt || "", negative: "",
};
}
// Background i2i render. Mirrors runInpaint's lifecycle, with two differences:
// no mask upload, and listeners are registered BEFORE the queue with a pending
// buffer that replays once prompt_id is known — a full-frame i2i can be served
// from cache (~0s), and an execution_success arriving before queuePrompt's
// response sets prompt_id would otherwise be dropped and the promise would hang
// (the bug just fixed in enlargeImageInBackground). runInpaint registers after
// the queue and lacks this guard; i2i must not inherit that race.
export function runI2IInBackground(prepared, options = {}) {
const tracker = createTracker();
const run = { cancelled: false, promptId: null, executionStarted: false, cleanup: null };
tracker.cancel = async () => {
if (run.cancelled) return;
run.cancelled = true;
tracker.emit({ phase: "cancelled" });
try {
if (run.promptId) {
if (run.executionStarted) await api.interrupt();
else {
await api.fetchApi("/queue", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ delete: [run.promptId] }),
});
run.cleanup?.();
}
}
} catch (e) {
console.warn("[PromptChain] i2i cancel failed", e);
}
};
(async () => {
const { data } = prepared;
tracker.emit({ phase: "building" });
let built;
try {
built = buildI2IGraph(data, options);
} catch (e) {
console.error("[PromptChain][i2i] build failed:", e);
tracker.emit({ phase: "error", message: `build failed: ${e.message}` });
toast("error", `i2i build failed: ${e.message}`);
return;
}
if (run.cancelled) return;
tracker.emit({ phase: "queueing" });
const workflowId = freshWorkflowId();
const workflowExtra = data.workflow ? { ...data.workflow, id: workflowId } : undefined;
let promptId = null;
let idKnown = false;
let meta = null;
let lastTemp = null;
let previewUrl = null;
const pending = [];
const setPreview = (blob) => {
if (previewUrl) URL.revokeObjectURL(previewUrl);
previewUrl = blob ? URL.createObjectURL(blob) : null;
return previewUrl;
};
const cleanup = () => {
api.removeEventListener("execution_start", onStart);
api.removeEventListener("progress", onProgress);
api.removeEventListener("b_preview", onPreview);
api.removeEventListener("executed", onExecuted);
api.removeEventListener("execution_success", onSuccess);
api.removeEventListener("execution_error", onError);
api.removeEventListener("execution_interrupted", onError);
if (promptId) externallyTrackedPrompts.delete(promptId);
setPreview(null);
};
run.cleanup = cleanup;
const takeStart = (d) => { if (d?.prompt_id === promptId) run.executionStarted = true; };
const takeProgress = (d) => {
if (d?.prompt_id !== promptId || run.cancelled) return;
run.executionStarted = true;
tracker.emit({ phase: "running", value: d.value, max: d.max, previewUrl });
};
const takeExecuted = (d) => {
if (d?.prompt_id !== promptId || !d?.output?.images?.length) return;
lastTemp = d.output.images[d.output.images.length - 1];
};
const takeSuccess = (d) => {
if (d?.prompt_id !== promptId) return;
cleanup();
if (run.cancelled) return;
if (!lastTemp) { tracker.emit({ phase: "error", message: "run produced no image" }); return; }
tracker.emit({ phase: "done", temp: lastTemp, meta, workflowId });
};
const takeError = (d) => {
if (d?.prompt_id !== promptId) return;
cleanup();
if (run.cancelled) return;
const message = d?.exception_message || "execution failed";
tracker.emit({ phase: "error", message });
toast("error", `i2i failed: ${message}`);
};
// b_preview carries no prompt_id and self-gates on run.executionStarted, so
// it's safe to handle live. The prompt_id-matching events buffer until the id
// is known, then replay.
const onStart = ({ detail }) => idKnown ? takeStart(detail) : pending.push(["start", detail]);
const onProgress = ({ detail }) => idKnown ? takeProgress(detail) : pending.push(["progress", detail]);
const onExecuted = ({ detail }) => idKnown ? takeExecuted(detail) : pending.push(["executed", detail]);
const onSuccess = ({ detail }) => idKnown ? takeSuccess(detail) : pending.push(["success", detail]);
const onError = ({ detail }) => idKnown ? takeError(detail) : pending.push(["error", detail]);
const onPreview = ({ detail }) => {
if (run.cancelled || !run.executionStarted || !(detail instanceof Blob)) return;
const url = setPreview(detail);
const s = tracker.state;
if (s.phase === "running") tracker.emit({ ...s, previewUrl: url });
};
api.addEventListener("execution_start", onStart);
api.addEventListener("progress", onProgress);
api.addEventListener("b_preview", onPreview);
api.addEventListener("executed", onExecuted);
api.addEventListener("execution_success", onSuccess);
api.addEventListener("execution_error", onError);
api.addEventListener("execution_interrupted", onError);
armExternalQueue();
try {
const previewMethod = app.extensionManager?.setting?.get?.("Comfy.Execution.PreviewMethod")
?? app.ui?.settings?.getSettingValue?.("Comfy.Execution.PreviewMethod")
?? "default";
const res = await api.queuePrompt(0, { output: built.graph, workflow: workflowExtra }, { previewMethod });
promptId = res?.prompt_id || null;
} catch (e) {
disarmExternalQueue();
cleanup();
const message = e?.response?.error?.message || e?.message || "queue rejected the prompt";
tracker.emit({ phase: "error", message });
toast("error", `i2i failed to queue: ${message}`);
return;
}
if (!promptId) {
disarmExternalQueue();
cleanup();
tracker.emit({ phase: "error", message: "queue returned no prompt id" });
return;
}
externallyTrackedPrompts.add(promptId);
disarmExternalQueue();
run.promptId = promptId;
meta = collectI2IMeta(built.graph, built.detailId, data, options);
if (run.cancelled) {
try {
await api.fetchApi("/queue", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ delete: [promptId] }),
});
} catch {}
cleanup();
return;
}
tracker.emit({ phase: "running" });
// The id is known — drain anything that arrived first (a cached run can have
// already fired success). executed before success so lastTemp is set.
idKnown = true;
for (const [k, d] of pending) if (k === "start") takeStart(d);
for (const [k, d] of pending) if (k === "progress") takeProgress(d);
for (const [k, d] of pending) if (k === "executed") takeExecuted(d);
for (const [k, d] of pending) { if (k === "success") takeSuccess(d); else if (k === "error") takeError(d); }
pending.length = 0;
})().catch((e) => {
console.error("[PromptChain] i2i run failed", e);
tracker.emit({ phase: "error", message: e?.message || "unexpected failure" });
toast("error", `i2i failed: ${e?.message || e}`);
});
return tracker;
}
+23 -4
View File
@@ -24,12 +24,12 @@ function toast(severity, detail) {
app.extensionManager?.toast?.add({ severity, summary: "Inpaint", detail, life: 7000 });
}
function freshWorkflowId() {
export function freshWorkflowId() {
if (globalThis.crypto?.randomUUID) return crypto.randomUUID();
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
}
function createTracker() {
export function createTracker() {
const listeners = new Set();
const tracker = {
state: { phase: "building" },
@@ -770,10 +770,24 @@ export function buildKrea2InpaintPrompt(data, options, maskName) {
return node?.widgets?.find((w) => w.name === widget)?.options?.values || [];
} catch { return []; }
};
// Realism mode (krea2_turbo): abliterated Qwen3-VL encoder + projector/realism
// LoRAs. VAE stays qwen_image_vae on purpose — MaskedDetail composites the
// re-detailed patch back into the qwen-decoded frame, so decoding it through
// wan_2.1 would shift colour/texture and leave a seam (same reason the krea2
// tile upscale keeps qwen_image_vae).
const realism = !!options.realism;
const clipOpts = probe("CLIPLoader", "clip_name");
const clipFile = clipOpts.find((o) => /qwen3vl.*4b/i.test(o)) || clipOpts.find((o) => /qwen3.?vl.*4b/i.test(o));
const loraOpts = probe("LoraLoaderModelOnly", "lora_name");
const clipFile = realism
? clipOpts.find((o) => /huihui.*qwen3.?vl.*4b.*abliterated|qwen3.?vl.*4b.*abliterated/i.test(o))
: (clipOpts.find((o) => /qwen3vl.*4b/i.test(o)) || clipOpts.find((o) => /qwen3.?vl.*4b/i.test(o)));
const vaeFile = probe("VAELoader", "vae_name").find((o) => /qwen.*image.*vae/i.test(o));
if (!clipFile || !vaeFile) throw new Error("the Krea 2 text encoder / VAE aren't installed");
const projLora = realism && loraOpts.find((o) => /krea2_turbo_projector_scale/i.test(o));
const realLora = realism && loraOpts.find((o) => /krea2-realism/i.test(o));
if (!clipFile || !vaeFile) throw new Error(realism
? "Realism mode needs the abliterated Qwen3-VL encoder installed (Huihui-Qwen3-VL-4B-Instruct-abliterated)"
: "the Krea 2 text encoder / VAE aren't installed");
if (realism && (!projLora || !realLora)) throw new Error("Realism mode needs the Krea2-realism-V1 + krea2_turbo_projector_scale LoRAs installed");
const graph = {};
let nextId = offscreenIdBase(); // render-node ids must stay off the live canvas — else execution images paint onto a live node sharing the id (see offscreen-graph.js)
@@ -808,6 +822,11 @@ export function buildKrea2InpaintPrompt(data, options, maskName) {
const msfId = add("ModelSamplingFlux", { model: [unetId, 0], max_shift: 0.90625, base_shift: 0.5, width: 1024, height: 1024 });
modelRef = [msfId, 0];
}
if (realism) {
const proj = add("LoraLoaderModelOnly", { model: modelRef, lora_name: projLora, strength_model: 0.5 });
const real = add("LoraLoaderModelOnly", { model: [proj, 0], lora_name: realLora, strength_model: 0.6 });
modelRef = [real, 0];
}
const detailId = add("PromptChain_MaskedDetail", {
model: modelRef, vae: [vaeId, 0],
image: [loadId, 0], mask: [maskId, 0],
+1 -1
View File
@@ -39,7 +39,7 @@ api.addEventListener("promptchain_recognition_done", () => {
const KSAMPLER_TYPES = new Set([
"KSampler", "KSamplerAdvanced",
"PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale",
"PromptChain_IdeogramSampler",
"PromptChain_IdeogramSampler", "ClownsharKSampler_Beta",
]);
const MODEL_WIDGET_NAMES = ["ckpt_name", "unet_name", "model_name"];
+131
View File
@@ -943,6 +943,136 @@ function injectPuLID(pcNode) {
return true;
}
// ── Krea 2 style LoRA (always-present "Style" panel section) ────────
// One swappable model-only LoRA on the sampler's model line, paired with a
// trigger word in the prompt. Selecting a style swaps both the LoRA file and
// the tracked trigger fragment; null removes the node and the trigger. The
// panel downloads the file first, so it's assumed on disk here (the combo-option
// push covers a lora list ComfyUI hasn't rescanned yet).
function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
function setStyleWidget(node, name, value, isCombo) {
const w = node.widgets?.find(x => x.name === name);
if (!w) return;
if (isCombo && w.options?.values && !w.options.values.includes(value)) w.options.values.push(value);
w.value = value;
w.callback?.(value);
}
function spliceStyleLora(graph, loraNode, ksNode) {
const modelIdx = ksNode.inputs?.findIndex(i => i.name === "model");
if (modelIdx == null || modelIdx < 0 || ksNode.inputs[modelIdx].link == null) return;
const link = getLink(graph, ksNode.inputs[modelIdx].link);
if (!link) return;
const source = graph.getNodeById(link.origin_id);
if (!source) return;
const sourceSlot = link.origin_slot ?? (source.outputs?.findIndex(o => o.type === "MODEL") ?? 0);
const loraIn = loraNode.inputs?.findIndex(i => i.name === "model") ?? 0;
ksNode.disconnectInput(modelIdx);
source.connect(sourceSlot, loraNode, loraIn);
loraNode.connect(0, ksNode, modelIdx);
}
function removeStyleLora(graph, loraNode, ksNode) {
// Reconnect the LoRA's upstream model source straight to the sampler so the
// model line survives the node's removal.
const loraIn = loraNode.inputs?.findIndex(i => i.name === "model") ?? 0;
const inLink = loraNode.inputs?.[loraIn]?.link;
const modelIdx = ksNode.inputs?.findIndex(i => i.name === "model");
if (inLink != null && modelIdx != null && modelIdx >= 0) {
const l = getLink(graph, inLink);
const source = l && graph.getNodeById(l.origin_id);
if (source) {
ksNode.disconnectInput(modelIdx);
source.connect(l.origin_slot ?? 0, ksNode, modelIdx);
}
}
graph.remove(loraNode);
}
// Keep a single tracked trigger (pcNode.properties.pcrStyleTrigger) in the
// positive half of the prompt. Swapping replaces the old fragment with the new;
// only the tracked substring is touched, never other user text.
function swapStyleTrigger(pcNode, newTrigger) {
const prev = pcNode.properties?.pcrStyleTrigger || "";
if (prev === (newTrigger || "")) return;
const view = pcNode._pcrEditor;
const promptWidget = pcNode.widgets?.find(w => w.name === "prompt");
const doc = view ? view.state.doc.toString() : String(promptWidget?.value || "");
// Never insert past the "Negative Prompt:" marker — triggers are positive.
const NEG_RE = /(^|\n)[ \t]*negative\s+prompt\s*:/i;
const m = doc.match(NEG_RE);
const splitAt = m ? m.index : doc.length;
let pos = doc.slice(0, splitAt);
const tail = doc.slice(splitAt);
const strip = (text, trig) => trig
? text.replace(new RegExp("(,\\s*)?" + escapeRegExp(trig) + "(\\s*,)?", "i"),
(_, a, b) => (a && b) ? ", " : "")
: text;
pos = strip(pos, prev);
if (newTrigger) pos = strip(pos, newTrigger); // guard against a pre-existing copy
pos = pos.replace(/,\s*,/g, ",").replace(/\s+$/, "");
if (newTrigger) pos = pos ? pos + ", " + newTrigger : newTrigger;
const next = tail ? (pos ? pos + tail : tail.replace(/^\n/, "")) : pos;
if (view) {
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: next } });
} else if (promptWidget) {
promptWidget.value = next;
promptWidget.callback?.(next);
}
pcNode.properties.pcrStyleTrigger = newTrigger || "";
}
// Apply / swap / clear the Krea 2 style LoRA. `lora` is a catalog entry
// {id, filename, trigger, ...} or null to remove. Returns true on success.
export function applyStyleLora(pcNode, lora, strength) {
const graph = pcNode.graph || app.graph;
if (!graph) return false;
const LG = window.LiteGraph || graph.constructor?.LiteGraph;
if (!LG) return false;
const ksNode = findDownstreamNodes(pcNode).find(
n => n.comfyClass === "KSampler" || n.comfyClass === "KSamplerAdvanced"
);
if (!ksNode) return false;
pcNode.properties = pcNode.properties || {};
const existing = (graph._nodes || []).find(
n => n.comfyClass === "LoraLoaderModelOnly" && n.properties?.pcrStyleLora
);
if (!lora) {
if (existing) removeStyleLora(graph, existing, ksNode);
swapStyleTrigger(pcNode, null);
pcNode.properties.pcrStyleLora = "";
graph.setDirtyCanvas(true, true);
return true;
}
let loraNode = existing;
if (!loraNode) {
loraNode = LG.createNode("LoraLoaderModelOnly");
if (!loraNode) return false;
loraNode.properties = loraNode.properties || {};
loraNode.properties.pcrStyleLora = true;
loraNode.title = "Style LoRA";
loraNode.pos = [pcNode.pos[0] + 420, pcNode.pos[1] + 520];
graph.add(loraNode);
spliceStyleLora(graph, loraNode, ksNode);
}
setStyleWidget(loraNode, "lora_name", lora.filename, true);
setStyleWidget(loraNode, "strength_model", strength, false);
swapStyleTrigger(pcNode, lora.trigger || null);
pcNode.properties.pcrStyleLora = lora.id || lora.filename;
graph.setDirtyCanvas(true, true);
return true;
}
// IPAdapter style transfer ("Style Reference", cubiq's ComfyUI_IPAdapter_plus).
// The UnifiedLoader bundles the ipadapter weights + CLIP-Vision encoder into
// one IPADAPTER pipe; IPAdapterAdvanced with weight_type "style transfer"
@@ -2703,6 +2833,7 @@ export async function showModelModal(pcNode, modelInfo, anchorEl, { tab = null }
closeModelModal();
window.dispatchEvent(new CustomEvent("pcr-open-model-picker", { detail: { nodeId: pcNode.id } }));
},
onApplyStyleLora: (lora, strength) => applyStyleLora(pcNode, lora, strength),
onInjectNode: async (type) => {
// ControlNet items arrive as "ControlNet:<controlType>" (depth/canny/…);
// the base type drives requirements + the install modal.
+114 -2
View File
@@ -667,6 +667,116 @@
border-radius: 5px; word-break: break-word;
}
.pcr-ip-modal.svelte-uprazu {
width: 96vw; height: 94vh;
min-width: 900px; max-width: 96vw;
display: flex; flex-direction: column;
}
.pcr-ip-body.svelte-uprazu { display: flex; gap: 16px; flex: 1; min-height: 0; }
.pcr-ip-left.svelte-uprazu { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pcr-ip-right.svelte-uprazu { flex: 0 0 320px; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 4px; }
.pcr-ip-tabs.svelte-uprazu { display: flex; align-items: center; margin-bottom: 8px; }
.pcr-ip-seg.svelte-uprazu { display: flex; border: 1px solid #3a3a3a; border-radius: 5px; overflow: hidden; }
.pcr-ip-tab.svelte-uprazu { padding: 4px 18px; font-size: 12px; color: #aaa; cursor: pointer; background: transparent; }
.pcr-ip-tab.svelte-uprazu + .pcr-ip-tab:where(.svelte-uprazu) { border-left: 1px solid #3a3a3a; }
.pcr-ip-tab.svelte-uprazu:hover:not(.active) { color: #ddd; }
.pcr-ip-tab.active.svelte-uprazu { background: #c85909; color: #fff; }
.pcr-ip-tools.svelte-uprazu { display: flex; align-items: center; gap: 8px; margin-left: auto; align-self: center; }
.pcr-ip-tool-btn.svelte-uprazu {
padding: 3px 10px; font-size: 11.5px; color: #aaa;
background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; cursor: pointer;
}
.pcr-ip-tool-btn.svelte-uprazu:hover { color: #fff; border-color: #555; }
.pcr-ip-tool-btn.on.svelte-uprazu { color: #fff; border-color: #c85909; background: rgba(200, 89, 9, 0.2); }
.pcr-ip-tool-btn.svelte-uprazu:disabled { opacity: 0.4; cursor: default; }
.pcr-ip-viewport.svelte-uprazu {
position: relative; overflow: hidden;
flex: 1; min-height: 0; background: #141414;
border: 1px solid #3a3a3a; border-radius: 6px;
touch-action: none; cursor: grab;
}
.pcr-ip-viewport.svelte-uprazu:active { cursor: grabbing; }
.pcr-ip-stage.svelte-uprazu { position: absolute; transform-origin: 0 0; }
.pcr-ip-stage.svelte-uprazu img:where(.svelte-uprazu) { display: block; user-select: none; pointer-events: none; }
.pcr-ip-split-before.svelte-uprazu { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
.pcr-ip-split-divider.svelte-uprazu {
position: absolute; top: 0; bottom: 0;
width: 18px; margin-left: -9px;
z-index: 7; cursor: ew-resize; touch-action: none;
}
.pcr-ip-split-divider.svelte-uprazu::before {
content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
width: 2px; margin-left: -1px;
background: rgba(255, 255, 255, 0.9); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.pcr-ip-split-knob.svelte-uprazu {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 30px; height: 30px; border-radius: 50%;
background: rgba(20, 20, 20, 0.85); border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center; color: #fff;
}
.pcr-ip-split-knob.svelte-uprazu svg:where(.svelte-uprazu) { width: 18px; height: 18px; }
.pcr-ip-split-label.svelte-uprazu {
position: absolute; top: 8px; z-index: 6;
padding: 2px 7px; font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
color: #fff; background: rgba(20, 20, 20, 0.7); border-radius: 4px;
pointer-events: none;
}
.pcr-ip-split-label.before.svelte-uprazu { left: 8px; }
.pcr-ip-split-label.after.svelte-uprazu { right: 8px; }
.pcr-ip-center.svelte-uprazu { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.pcr-ip-live-preview.svelte-uprazu { max-width: 100%; max-height: 100%; object-fit: contain; }
.pcr-ip-waiting.svelte-uprazu { font-size: 12.5px; color: #777; }
.pcr-ip-combos.svelte-uprazu { display: flex; gap: 10px; }
.pcr-ip-combo.svelte-uprazu { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pcr-ip-select.svelte-uprazu {
width: 100%; box-sizing: border-box;
padding: 5px 8px; font-size: 12px; color: #999;
background: #1c1c1c; border: 1px solid #3a3a3a; border-radius: 5px;
outline: none; cursor: pointer;
}
.pcr-ip-select.at-rec.svelte-uprazu { color: #5ed357; border-color: rgba(94, 211, 87, 0.45); }
.pcr-ip-progress.svelte-uprazu { position: absolute; left: 16px; right: 16px; bottom: 12px; display: flex; align-items: center; gap: 10px; }
.pcr-ip-bar.svelte-uprazu { flex: 1; height: 6px; border-radius: 3px; background: rgba(0, 0, 0, 0.55); border: 1px solid #3a3a3a; overflow: hidden; }
.pcr-ip-bar-fill.svelte-uprazu { height: 100%; background: #c85909; transition: width 0.2s ease; }
.pcr-ip-bar-fill.indeterminate.svelte-uprazu { width: 35%; animation: svelte-uprazu-pcr-i2i-slide 1.2s ease-in-out infinite alternate; }
@keyframes svelte-uprazu-pcr-i2i-slide { from { margin-left: 0; } to { margin-left: 65%; } }
.pcr-ip-bar-label.svelte-uprazu { font-size: 10.5px; color: #bbb; }
.pcr-ip-label.svelte-uprazu { font-size: 11px; color: #888; margin-top: 6px; }
.pcr-ip-adv-head.svelte-uprazu { margin-top: 8px; font-size: 11px; color: #888; cursor: pointer; user-select: none; }
.pcr-ip-adv-head.svelte-uprazu:hover { color: #ccc; }
.pcr-ip-hint.svelte-uprazu { font-size: 11px; color: #777; }
.pcr-ip-prompt.svelte-uprazu {
min-height: 200px; resize: vertical;
padding: 8px 10px; font-size: 12.5px; line-height: 1.45; color: #ddd;
background: #1c1c1c; border: 1px solid #3a3a3a; border-radius: 5px;
outline: none; font-family: inherit;
}
.pcr-ip-prompt.svelte-uprazu:focus { border-color: #c85909; }
.pcr-ip-prompt-editor.svelte-uprazu {
height: 220px; padding: 0; resize: none; overflow: hidden;
display: flex; flex-direction: column;
}
.pcr-ip-prompt-editor.svelte-uprazu:focus-within { border-color: #c85909; }
.pcr-ip-prompt-editor.svelte-uprazu .cm-editor { flex: 1; min-height: 0; height: 100%; }
.pcr-ip-prompt-editor.svelte-uprazu .cm-scroller { overflow: auto; }
.pcr-ip-refprompt.svelte-uprazu {
height: 110px; resize: vertical;
padding: 7px 10px; font-size: 11.5px; line-height: 1.4; color: #9a9a9a;
background: #161616; border: 1px solid #313131; border-radius: 5px;
outline: none; font-family: inherit; cursor: text; user-select: text;
}
.pcr-ip-denoise.svelte-uprazu { display: flex; align-items: center; gap: 10px; }
.pcr-ip-denoise.svelte-uprazu .pcr-ip-label:where(.svelte-uprazu) { flex: none; width: 52px; margin-top: 0; }
.pcr-modal-btn.svelte-uprazu:disabled { opacity: 0.45; cursor: default; }
.pcr-ip-error.svelte-uprazu {
margin: 0 2px 2px; padding: 7px 12px;
font-size: 12px; color: #ff8a80; line-height: 1.4;
background: rgba(229, 57, 53, 0.12); border: 1px solid rgba(229, 57, 53, 0.4);
border-radius: 5px; word-break: break-word;
}
.pcr-adj-backdrop.svelte-k7hhb3 { z-index: 10010; }
.pcr-adj-modal.svelte-k7hhb3 {
width: 97vw; height: 95vh;
@@ -980,8 +1090,9 @@
}*/
/* (unused) .pcr-ed-ip-prompt:focus { border-color: #c85909; }*/
/* (unused) .pcr-ed-ip-row { display: flex; align-items: center; gap: 10px; margin: 4px 0; }*/
.pcr-ed-render-row.svelte-1ozzqkm { display: flex; gap: 8px; }
.pcr-ed-render-row.svelte-1ozzqkm .pcr-ed-ip-render:where(.svelte-1ozzqkm) { flex: 1; margin-top: 0; width: auto; white-space: nowrap; }
.pcr-ed-render-row.svelte-1ozzqkm { display: flex; flex-wrap: wrap; gap: 8px; }
/* ~50% basis → two buttons per row (a lone trailing button grows to full). */
.pcr-ed-render-row.svelte-1ozzqkm .pcr-ed-ip-render:where(.svelte-1ozzqkm) { flex: 1 1 calc(50% - 4px); margin-top: 0; width: auto; white-space: nowrap; }
.pcr-ed-ip-render.svelte-1ozzqkm {
width: 100%; margin-top: 6px; padding: 7px 10px; font-size: 12.5px; color: #fff; cursor: pointer;
background: #c85909; border: none; border-radius: 5px;
@@ -6706,6 +6817,7 @@
letter-spacing: 0;
}
.pcr-section-expand-toggle.svelte-82atyh:hover { color: #999; }
.pcr-style-status.svelte-82atyh { font-size: 11px; color: #5dcaff; }
/* widget rows */
.pcr-model-panel-row.svelte-82atyh {
+312 -105
View File
@@ -1,6 +1,7 @@
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, I as to_array, a1 as set_selected, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { d as delegate, p as push, a as prop, s as state, c as proxy, u as user_effect, e as set, f as sibling, o as bind_this, t as template_effect, g as get, x as set_attribute, q as set_value, j as delegated, k as append, l as pop, m as user_derived, n as child, A as from_html, w as each, z as index, i as if_block, G as set_checked, y as set_text, h as set_class, v as first_child, D as comment, a1 as set_selected, I as to_array, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
import { s as set_style } from "./style-Boi27oOu.js";
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-cVxhUf51.js";
import { api } from "/scripts/api.js";
import { S as SettingsSlider } from "./SettingsSlider-CKF_XmgB.js";
import { b as bind_value } from "./input-DFQhebEz.js";
import { b as bind_select_value } from "./select-Dp4ExMMc.js";
@@ -197,24 +198,29 @@ var root_28 = from_html(`<span style="color:#555;margin:0 2px">/</span> <div cla
var root_24$1 = from_html(`<div class="pcr-model-panel-combo-wrap svelte-82atyh"><!> <select></select></div> <!>`, 1);
var root_18$1 = from_html(`<div><span class="pcr-model-panel-label svelte-82atyh"> </span> <!></div>`);
var root_2$5 = from_html(`<div class="pcr-model-panel-section svelte-82atyh"><div class="pcr-model-panel-section-title svelte-82atyh"> <!></div> <!> <!> <!></div>`);
var root_30 = from_html(`<div class="pcr-settings-save-error svelte-82atyh"> </div>`);
var root_31 = from_html(`<button class="pcr-model-panel-apply">Cancel</button> <button class="pcr-model-panel-save">Save Ranges</button>`, 1);
var root_36 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_35 = from_html(`<div class="pcr-footer-dropdown-item pcr-footer-dropdown-back svelte-82atyh"> </div> <!>`, 1);
var root_39 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_31 = from_html(`<option> </option>`);
var root_32 = from_html(`<div class="pcr-model-panel-row svelte-82atyh"><span class="pcr-model-panel-label svelte-82atyh">strength</span> <!></div>`);
var root_33 = from_html(`<div class="pcr-model-panel-row svelte-82atyh"><span class="pcr-style-status svelte-82atyh"> </span></div>`);
var root_30 = from_html(`<div class="pcr-model-panel-section svelte-82atyh"><div class="pcr-model-panel-section-title svelte-82atyh">Style</div> <div class="pcr-model-panel-row svelte-82atyh"><span class="pcr-model-panel-label svelte-82atyh">LoRA</span> <div class="pcr-model-panel-combo-wrap svelte-82atyh"><select class="pcr-model-panel-select svelte-82atyh"><option>None</option><!></select></div></div> <!> <!></div>`);
var root_34 = from_html(`<div class="pcr-settings-save-error svelte-82atyh"> </div>`);
var root_35 = from_html(`<button class="pcr-model-panel-apply">Cancel</button> <button class="pcr-model-panel-save">Save Ranges</button>`, 1);
var root_40 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_41 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_34 = from_html(`<div class="pcr-footer-dropdown-menu svelte-82atyh" style="display:block;position:absolute;bottom:100%;left:0;min-width:100%"><!></div>`);
var root_33 = from_html(`<div style="position:relative"><button class="pcr-model-panel-apply">Add ▾</button> <!></div>`);
var root_43 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh">Restore Defaults</div>`);
var root_44 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh">Reset to System Defaults</div>`);
var root_42 = from_html(`<div class="pcr-footer-dropdown-menu svelte-82atyh" style="display:block;position:absolute;bottom:100%;left:0;min-width:100%"><div class="pcr-footer-dropdown-item svelte-82atyh">Set Ranges</div> <!> <!></div>`);
var root_32 = from_html(`<!> <div style="position:relative"><button class="pcr-model-panel-apply">Edit ▾</button> <!></div> <button class="pcr-model-panel-save">Save as Default</button>`, 1);
var root$5 = from_html(`<div class="pcr-model-panel-body"><!> <!></div> <!> <div class="pcr-model-panel-footer svelte-82atyh"><!></div>`, 1);
var root_39 = from_html(`<div class="pcr-footer-dropdown-item pcr-footer-dropdown-back svelte-82atyh"> </div> <!>`, 1);
var root_43 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_44 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_45 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh"> </div>`);
var root_38 = from_html(`<div class="pcr-footer-dropdown-menu svelte-82atyh" style="display:block;position:absolute;bottom:100%;left:0;min-width:100%"><!></div>`);
var root_37 = from_html(`<div style="position:relative"><button class="pcr-model-panel-apply">Add ▾</button> <!></div>`);
var root_47 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh">Restore Defaults</div>`);
var root_48 = from_html(`<div class="pcr-footer-dropdown-item svelte-82atyh">Reset to System Defaults</div>`);
var root_46 = from_html(`<div class="pcr-footer-dropdown-menu svelte-82atyh" style="display:block;position:absolute;bottom:100%;left:0;min-width:100%"><div class="pcr-footer-dropdown-item svelte-82atyh">Set Ranges</div> <!> <!></div>`);
var root_36 = from_html(`<!> <div style="position:relative"><button class="pcr-model-panel-apply">Edit ▾</button> <!></div> <button class="pcr-model-panel-save">Save as Default</button>`, 1);
var root$5 = from_html(`<div class="pcr-model-panel-body"><!> <!> <!></div> <!> <div class="pcr-model-panel-footer svelte-82atyh"><!></div>`, 1);
function SettingsTab($$anchor, $$props) {
var _a, _b, _c;
var _a, _b, _c, _d, _e;
push($$props, true);
let detected = prop($$props, "detected", 19, () => []), savedConfig = prop($$props, "savedConfig", 3, null), rangeMode = prop($$props, "rangeMode", 3, false), expandedSections = prop($$props, "expandedSections", 23, () => ({}));
let detected = prop($$props, "detected", 19, () => []), savedConfig = prop($$props, "savedConfig", 3, null), rangeMode = prop($$props, "rangeMode", 3, false), expandedSections = prop($$props, "expandedSections", 23, () => ({})), pcNode = prop($$props, "pcNode", 3, null), architecture = prop($$props, "architecture", 3, ""), family = prop($$props, "family", 3, ""), onApplyStyleLora = prop($$props, "onApplyStyleLora", 3, () => {
});
const DISPLAY_NAMES = {
sampler_name: "Sampler",
stop_at_clip_layer: "Last Layer",
@@ -474,6 +480,128 @@ function SettingsTab($$anchor, $$props) {
null
// the group (e.g. ControlNet) drilled into
);
const showStyleSection = user_derived(() => architecture() === "krea2" && family() === "krea2_turbo");
let styleCatalog = state(proxy([]));
let styleInstalled = state(proxy(
{}
// filename → exists
));
let styleSelected = state(proxy(((_e = (_d = pcNode()) == null ? void 0 : _d.properties) == null ? void 0 : _e.pcrStyleLora) || ""));
let styleStrength = state(1);
let styleBusy = state(false);
let styleStatus = state("");
user_effect(() => {
if (!get(showStyleSection)) return;
let cancelled = false;
(async () => {
try {
const res = await fetch(`/promptchain/loras/catalog?architecture=${encodeURIComponent(architecture())}&family=${encodeURIComponent(family())}`);
const data = await res.json();
if (cancelled) return;
set(styleCatalog, data.loras || [], true);
const cur = get(styleCatalog).find((l) => l.id === get(styleSelected));
if (cur) set(styleStrength, cur.strength_default ?? 1, true);
await refreshStyleInstalled();
} catch (e) {
console.warn("[PromptChain] style catalog load failed:", e);
}
})();
return () => {
cancelled = true;
};
});
async function refreshStyleInstalled() {
if (!get(styleCatalog).length) return;
try {
const res = await fetch("/promptchain/models/check-files", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
files: get(styleCatalog).map((l) => ({
filename: l.filename,
folder: l.folder || "loras",
size_bytes: l.size_bytes || 0
}))
})
});
const data = await res.json();
const map = {};
for (const r of data.results || []) map[r.filename] = r.exists;
set(styleInstalled, map, true);
} catch (e) {
console.warn("[PromptChain] style install check failed:", e);
}
}
function downloadStyle(lora) {
return new Promise((resolve) => {
const s = lora.source || {};
const url = s.provider === "huggingface" ? `https://huggingface.co/${s.repo}/resolve/main/${s.path}` : s.download_url || s.url || "";
if (!url) {
resolve(false);
return;
}
const cleanup = () => {
api.removeEventListener("promptchain_download_progress", onProgress);
api.removeEventListener("promptchain_download_done", onDone);
};
function onProgress({ detail }) {
if (detail.filename !== lora.filename) return;
set(styleStatus, `Downloading ${lora.label}: ${detail.progress}%`);
}
function onDone({ detail }) {
if (detail.filename !== lora.filename) return;
cleanup();
resolve(detail.status === "completed");
}
api.addEventListener("promptchain_download_progress", onProgress);
api.addEventListener("promptchain_download_done", onDone);
fetch("/promptchain/civitai/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, filename: lora.filename, folder: lora.folder || "loras" })
}).then((r) => r.json()).then((d) => {
if (d.error) {
cleanup();
set(styleStatus, d.error, true);
resolve(false);
}
}).catch((err) => {
cleanup();
set(styleStatus, err.message, true);
resolve(false);
});
});
}
async function onStyleChange(id) {
var _a2, _b2;
set(styleSelected, id, true);
if (!id) {
onApplyStyleLora()(null, get(styleStrength));
set(styleStatus, "");
return;
}
const lora = get(styleCatalog).find((l) => l.id === id);
if (!lora) return;
set(styleStrength, lora.strength_default ?? get(styleStrength) ?? 1, true);
if (!get(styleInstalled)[lora.filename]) {
set(styleBusy, true);
const ok = await downloadStyle(lora);
set(styleBusy, false);
if (!ok) {
set(styleStatus, get(styleStatus) || "Download failed", true);
set(styleSelected, ((_b2 = (_a2 = pcNode()) == null ? void 0 : _a2.properties) == null ? void 0 : _b2.pcrStyleLora) || "", true);
return;
}
set(styleInstalled, { ...get(styleInstalled), [lora.filename]: true }, true);
set(styleStatus, "");
}
onApplyStyleLora()(lora, get(styleStrength));
}
function onStyleStrength(v) {
set(styleStrength, v, true);
const lora = get(styleCatalog).find((l) => l.id === get(styleSelected));
if (lora && get(styleInstalled)[lora.filename]) onApplyStyleLora()(lora, v);
}
let editMenuOpen = state(false);
let injectable = user_derived(() => $$props.injectableNodes());
var fragment = root$5();
@@ -1064,23 +1192,90 @@ function SettingsTab($$anchor, $$props) {
template_effect(() => set_text(text2, `${(config().label || type()) ?? ""} `));
append($$anchor2, div_2);
});
var node_24 = sibling(div, 2);
var node_24 = sibling(node_2, 2);
{
var consequent_20 = ($$anchor2) => {
var consequent_22 = ($$anchor2) => {
var div_16 = root_30();
var text_11 = child(div_16);
template_effect(() => set_text(text_11, `Save failed: ${get(saveError) ?? ""}`));
var div_17 = sibling(child(div_16), 2);
var div_18 = sibling(child(div_17), 2);
var select_3 = child(div_18);
var option_3 = child(select_3);
option_3.value = option_3.__value = "";
var node_25 = sibling(option_3);
each(node_25, 17, () => get(styleCatalog), index, ($$anchor3, l) => {
var option_4 = root_31();
var text_11 = child(option_4);
var option_4_value = {};
template_effect(() => {
set_selected(option_4, get(l).id === get(styleSelected));
set_text(text_11, `${get(l).label ?? ""}${get(styleInstalled)[get(l).filename] ? "" : " ⬇"}`);
if (option_4_value !== (option_4_value = get(l).id)) {
option_4.value = (option_4.__value = get(l).id) ?? "";
}
});
append($$anchor3, option_4);
});
var node_26 = sibling(div_17, 2);
{
var consequent_20 = ($$anchor3) => {
var div_19 = root_32();
var node_27 = sibling(child(div_19), 2);
SettingsSlider(node_27, {
min: 0,
max: 1.5,
step: 0.05,
get value() {
return get(styleStrength);
},
onChange: (v) => onStyleStrength(v)
});
append($$anchor3, div_19);
};
if_block(node_26, ($$render) => {
if (get(styleSelected)) $$render(consequent_20);
});
}
var node_28 = sibling(node_26, 2);
{
var consequent_21 = ($$anchor3) => {
var div_20 = root_33();
var span_5 = child(div_20);
var text_12 = child(span_5);
template_effect(() => set_text(text_12, get(styleStatus)));
append($$anchor3, div_20);
};
if_block(node_28, ($$render) => {
if (get(styleStatus)) $$render(consequent_21);
});
}
template_effect(() => {
select_3.disabled = get(styleBusy);
set_selected(option_3, !get(styleSelected));
});
delegated("change", select_3, (e) => onStyleChange(e.target.value));
append($$anchor2, div_16);
};
if_block(node_24, ($$render) => {
if (get(saveError)) $$render(consequent_20);
if (get(showStyleSection)) $$render(consequent_22);
});
}
var div_17 = sibling(node_24, 2);
var node_25 = child(div_17);
var node_29 = sibling(div, 2);
{
var consequent_21 = ($$anchor2) => {
var fragment_17 = root_31();
var consequent_23 = ($$anchor2) => {
var div_21 = root_34();
var text_13 = child(div_21);
template_effect(() => set_text(text_13, `Save failed: ${get(saveError) ?? ""}`));
append($$anchor2, div_21);
};
if_block(node_29, ($$render) => {
if (get(saveError)) $$render(consequent_23);
});
}
var div_22 = sibling(node_29, 2);
var node_30 = child(div_22);
{
var consequent_24 = ($$anchor2) => {
var fragment_17 = root_35();
var button_2 = first_child(fragment_17);
var button_3 = sibling(button_2, 2);
delegated("click", button_2, (e) => {
@@ -1091,37 +1286,37 @@ function SettingsTab($$anchor, $$props) {
append($$anchor2, fragment_17);
};
var alternate_4 = ($$anchor2) => {
var fragment_18 = root_32();
var node_26 = first_child(fragment_18);
var fragment_18 = root_36();
var node_31 = first_child(fragment_18);
{
var consequent_26 = ($$anchor3) => {
var div_18 = root_33();
var button_4 = child(div_18);
var node_27 = sibling(button_4, 2);
var consequent_29 = ($$anchor3) => {
var div_23 = root_37();
var button_4 = child(div_23);
var node_32 = sibling(button_4, 2);
{
var consequent_25 = ($$anchor4) => {
var div_19 = root_34();
var node_28 = child(div_19);
var consequent_28 = ($$anchor4) => {
var div_24 = root_38();
var node_33 = child(div_24);
{
var consequent_22 = ($$anchor5) => {
var fragment_19 = root_35();
var div_20 = first_child(fragment_19);
var text_12 = child(div_20);
var node_29 = sibling(div_20, 2);
each(node_29, 17, () => get(addSubmenu).children, index, ($$anchor6, child$1) => {
var div_21 = root_36();
var text_13 = child(div_21);
template_effect(() => set_text(text_13, get(child$1).label));
delegated("click", div_21, (e) => {
var consequent_25 = ($$anchor5) => {
var fragment_19 = root_39();
var div_25 = first_child(fragment_19);
var text_14 = child(div_25);
var node_34 = sibling(div_25, 2);
each(node_34, 17, () => get(addSubmenu).children, index, ($$anchor6, child$1) => {
var div_26 = root_40();
var text_15 = child(div_26);
template_effect(() => set_text(text_15, get(child$1).label));
delegated("click", div_26, (e) => {
e.stopPropagation();
set(addMenuOpen, false);
set(addSubmenu, null);
$$props.onInjectNode(get(child$1).value);
});
append($$anchor6, div_21);
append($$anchor6, div_26);
});
template_effect(() => set_text(text_12, `‹ ${get(addSubmenu).label ?? ""}`));
delegated("click", div_20, (e) => {
template_effect(() => set_text(text_14, `‹ ${get(addSubmenu).label ?? ""}`));
delegated("click", div_25, (e) => {
e.stopPropagation();
set(addSubmenu, null);
});
@@ -1129,46 +1324,46 @@ function SettingsTab($$anchor, $$props) {
};
var alternate_3 = ($$anchor5) => {
var fragment_20 = comment();
var node_30 = first_child(fragment_20);
each(node_30, 17, () => get(injectable), index, ($$anchor6, item) => {
var node_35 = first_child(fragment_20);
each(node_35, 17, () => get(injectable), index, ($$anchor6, item) => {
var fragment_21 = comment();
var node_31 = first_child(fragment_21);
var node_36 = first_child(fragment_21);
{
var consequent_23 = ($$anchor7) => {
var div_22 = root_39();
var text_14 = child(div_22);
template_effect(() => set_text(text_14, get(item)));
delegated("click", div_22, (e) => {
var consequent_26 = ($$anchor7) => {
var div_27 = root_43();
var text_16 = child(div_27);
template_effect(() => set_text(text_16, get(item)));
delegated("click", div_27, (e) => {
e.stopPropagation();
set(addMenuOpen, false);
$$props.onInjectNode(get(item));
});
append($$anchor7, div_22);
append($$anchor7, div_27);
};
var consequent_24 = ($$anchor7) => {
var div_23 = root_40();
var text_15 = child(div_23);
template_effect(() => set_text(text_15, `${get(item).label ?? ""} ▸`));
delegated("click", div_23, (e) => {
var consequent_27 = ($$anchor7) => {
var div_28 = root_44();
var text_17 = child(div_28);
template_effect(() => set_text(text_17, `${get(item).label ?? ""} ▸`));
delegated("click", div_28, (e) => {
e.stopPropagation();
set(addSubmenu, get(item), true);
});
append($$anchor7, div_23);
append($$anchor7, div_28);
};
var alternate_2 = ($$anchor7) => {
var div_24 = root_41();
var text_16 = child(div_24);
template_effect(() => set_text(text_16, get(item).label));
delegated("click", div_24, (e) => {
var div_29 = root_45();
var text_18 = child(div_29);
template_effect(() => set_text(text_18, get(item).label));
delegated("click", div_29, (e) => {
e.stopPropagation();
set(addMenuOpen, false);
$$props.onInjectNode(get(item).value);
});
append($$anchor7, div_24);
append($$anchor7, div_29);
};
if_block(node_31, ($$render) => {
if (typeof get(item) === "string") $$render(consequent_23);
else if (get(item).children) $$render(consequent_24, 1);
if_block(node_36, ($$render) => {
if (typeof get(item) === "string") $$render(consequent_26);
else if (get(item).children) $$render(consequent_27, 1);
else $$render(alternate_2, -1);
});
}
@@ -1176,15 +1371,15 @@ function SettingsTab($$anchor, $$props) {
});
append($$anchor5, fragment_20);
};
if_block(node_28, ($$render) => {
if (get(addSubmenu)) $$render(consequent_22);
if_block(node_33, ($$render) => {
if (get(addSubmenu)) $$render(consequent_25);
else $$render(alternate_3, -1);
});
}
append($$anchor4, div_19);
append($$anchor4, div_24);
};
if_block(node_27, ($$render) => {
if (get(addMenuOpen)) $$render(consequent_25);
if_block(node_32, ($$render) => {
if (get(addMenuOpen)) $$render(consequent_28);
});
}
delegated("click", button_4, (e) => {
@@ -1193,63 +1388,63 @@ function SettingsTab($$anchor, $$props) {
set(addSubmenu, null);
set(editMenuOpen, false);
});
append($$anchor3, div_18);
append($$anchor3, div_23);
};
if_block(node_26, ($$render) => {
if (get(injectable).length) $$render(consequent_26);
if_block(node_31, ($$render) => {
if (get(injectable).length) $$render(consequent_29);
});
}
var div_25 = sibling(node_26, 2);
var button_5 = child(div_25);
var node_32 = sibling(button_5, 2);
var div_30 = sibling(node_31, 2);
var button_5 = child(div_30);
var node_37 = sibling(button_5, 2);
{
var consequent_29 = ($$anchor3) => {
var div_26 = root_42();
var div_27 = child(div_26);
var node_33 = sibling(div_27, 2);
var consequent_32 = ($$anchor3) => {
var div_31 = root_46();
var div_32 = child(div_31);
var node_38 = sibling(div_32, 2);
{
var consequent_27 = ($$anchor4) => {
var div_28 = root_43();
delegated("click", div_28, (e) => {
var consequent_30 = ($$anchor4) => {
var div_33 = root_47();
delegated("click", div_33, (e) => {
e.stopPropagation();
set(editMenuOpen, false);
handleApply();
});
append($$anchor4, div_28);
append($$anchor4, div_33);
};
if_block(node_33, ($$render) => {
if (savedConfig()) $$render(consequent_27);
if_block(node_38, ($$render) => {
if (savedConfig()) $$render(consequent_30);
});
}
var node_34 = sibling(node_33, 2);
var node_39 = sibling(node_38, 2);
{
var consequent_28 = ($$anchor4) => {
var div_29 = root_44();
delegated("click", div_29, async (e) => {
var consequent_31 = ($$anchor4) => {
var div_34 = root_48();
delegated("click", div_34, async (e) => {
var _a2;
e.stopPropagation();
set(editMenuOpen, false);
await fetch(`/promptchain/models/settings/${((_a2 = savedConfig()) == null ? void 0 : _a2._hash) || ""}`, { method: "DELETE" });
$$props.onReopen({});
});
append($$anchor4, div_29);
append($$anchor4, div_34);
};
if_block(node_34, ($$render) => {
if (userSaved && hasSystem) $$render(consequent_28);
if_block(node_39, ($$render) => {
if (userSaved && hasSystem) $$render(consequent_31);
});
}
delegated("click", div_27, (e) => {
delegated("click", div_32, (e) => {
e.stopPropagation();
set(editMenuOpen, false);
$$props.onReopen({ rangeMode: true });
});
append($$anchor3, div_26);
append($$anchor3, div_31);
};
if_block(node_32, ($$render) => {
if (get(editMenuOpen)) $$render(consequent_29);
if_block(node_37, ($$render) => {
if (get(editMenuOpen)) $$render(consequent_32);
});
}
var button_6 = sibling(div_25, 2);
var button_6 = sibling(div_30, 2);
template_effect(() => button_6.disabled = get(saveDisabled));
delegated("click", button_5, (e) => {
e.stopPropagation();
@@ -1259,8 +1454,8 @@ function SettingsTab($$anchor, $$props) {
delegated("click", button_6, handleSave);
append($$anchor2, fragment_18);
};
if_block(node_25, ($$render) => {
if (rangeMode()) $$render(consequent_21);
if_block(node_30, ($$render) => {
if (rangeMode()) $$render(consequent_24);
else $$render(alternate_4, -1);
});
}
@@ -2996,6 +3191,18 @@ function ModelSettingsModal($$anchor, $$props) {
get onInjectNode() {
return $$props.onInjectNode;
},
get pcNode() {
return $$props.pcNode;
},
get architecture() {
return get(detectedArch);
},
get family() {
return get(currentFamily);
},
get onApplyStyleLora() {
return $$props.onApplyStyleLora;
},
get onClose() {
return $$props.onClose;
}
File diff suppressed because it is too large Load Diff
+48 -12
View File
@@ -321,7 +321,14 @@ export function enlargeImageInBackground(inputRef, targetW, targetH, engine = "u
const workflowId = globalThis.crypto?.randomUUID ? crypto.randomUUID() : `${Date.now().toString(16)}-bg`;
return new Promise((resolve, reject) => {
let promptId = null;
let settled = false;
let lastImage = null;
// A cached enlarge executes in ~0s, so its execution_success can reach the
// listener BEFORE queuePrompt's response resolves our prompt_id. Matching a
// real event against a still-null promptId silently drops it and the promise
// hangs forever — which strands the Edit "Add to Edit" handback awaiting it.
// Buffer every event until the id is known, then replay against it.
const pending = [];
const cleanup = () => {
api.removeEventListener("executed", onExecuted);
api.removeEventListener("execution_success", onSuccess);
@@ -329,21 +336,26 @@ export function enlargeImageInBackground(inputRef, targetW, targetH, engine = "u
api.removeEventListener("execution_interrupted", onError);
if (promptId) externallyTrackedPrompts.delete(promptId);
};
const onExecuted = ({ detail }) => {
const settle = (fn, arg) => { if (settled) return; settled = true; cleanup(); fn(arg); };
const takeExecuted = (detail) => {
if (detail?.prompt_id !== promptId || !detail?.output?.images?.length) return;
lastImage = detail.output.images[detail.output.images.length - 1];
};
const onSuccess = ({ detail }) => {
if (detail?.prompt_id !== promptId) return;
cleanup();
if (lastImage) resolve(lastImage);
else reject(new Error("background enlarge produced no image"));
const takeSuccess = (detail) => {
if (detail?.prompt_id !== promptId || settled) return;
if (lastImage) { settle(resolve, lastImage); return; }
// A fully-cached run can emit execution_success without re-emitting its
// executed (no image over the wire) — recover the output from history.
recoverEnlargeFromHistory(promptId).then((img) =>
img ? settle(resolve, img) : settle(reject, new Error("background enlarge produced no image")));
};
const onError = ({ detail }) => {
const takeError = (detail) => {
if (detail?.prompt_id !== promptId) return;
cleanup();
reject(new Error(detail?.exception_message || "background enlarge failed"));
settle(reject, new Error(detail?.exception_message || "background enlarge failed"));
};
const onExecuted = ({ detail }) => promptId ? takeExecuted(detail) : pending.push(["executed", detail]);
const onSuccess = ({ detail }) => promptId ? takeSuccess(detail) : pending.push(["success", detail]);
const onError = ({ detail }) => promptId ? takeError(detail) : pending.push(["error", detail]);
api.addEventListener("executed", onExecuted);
api.addEventListener("execution_success", onSuccess);
api.addEventListener("execution_error", onError);
@@ -354,16 +366,40 @@ export function enlargeImageInBackground(inputRef, targetW, targetH, engine = "u
promptId = res?.prompt_id || null;
if (promptId) externallyTrackedPrompts.add(promptId);
disarmExternalQueue();
if (!promptId) { cleanup(); reject(new Error("queue returned no prompt id")); }
if (!promptId) { settle(reject, new Error("queue returned no prompt id")); return; }
// Replay buffered events now the id is known — executed first so lastImage
// is set before any buffered success reads it.
for (const [kind, detail] of pending) if (kind === "executed") takeExecuted(detail);
for (const [kind, detail] of pending) {
if (kind === "success") takeSuccess(detail);
else if (kind === "error") takeError(detail);
}
pending.length = 0;
})
.catch((e) => {
disarmExternalQueue();
cleanup();
reject(new Error(e?.response?.error?.message || e?.message || "queue rejected the enlarge prompt"));
settle(reject, new Error(e?.response?.error?.message || e?.message || "queue rejected the enlarge prompt"));
});
});
}
// Last output image of a finished prompt, read straight from history — the net
// for a cached enlarge whose execution_success carries no image.
async function recoverEnlargeFromHistory(promptId) {
try {
const res = await api.fetchApi(`/history/${promptId}`);
const outputs = (await res.json())?.[promptId]?.outputs || {};
let last = null;
for (const nodeId of Object.keys(outputs)) {
const imgs = outputs[nodeId]?.images;
if (imgs?.length) last = imgs[imgs.length - 1];
}
return last;
} catch {
return null;
}
}
// ── enlarged-base cache (NoUpscale tile engines) ───────────────────────────
// The UltraSharp/SeedVR2 enlarge ("pre-process") is deterministic, so a retry
// that only changes tile dials reuses the saved base instead of re-running it.
+30 -4
View File
@@ -1869,8 +1869,13 @@ function buildKrea2TileStage(graph, LG, place, opts) {
if (unetFile && Array.isArray(unetOpts) && !unetOpts.includes(unetFile)) unetOpts.push(unetFile);
if (!unetFile) unetFile = unetOpts.find((o) => /krea2/i.test(o));
const clip = LG.createNode("CLIPLoader");
const clipFile = (clip?.widgets?.find((x) => x.name === "clip_name")?.options?.values || [])
.find((o) => /qwen3vl.*4b|qwen3.?vl.*4b/i.test(o));
// Realism mode (krea2_turbo): abliterated Qwen3-VL encoder. The VAE stays
// qwen_image_vae below — a tile decoded through wan_2.1 would seam against the
// qwen-decoded surround (see the VAE note).
const clipNames = clip?.widgets?.find((x) => x.name === "clip_name")?.options?.values || [];
const clipFile = opts.realism
? clipNames.find((o) => /huihui.*qwen3.?vl.*4b.*abliterated|qwen3.?vl.*4b.*abliterated/i.test(o))
: clipNames.find((o) => /qwen3vl.*4b|qwen3.?vl.*4b/i.test(o));
const vae = LG.createNode("VAELoader");
// Use Krea 2's native VAE (qwen_image_vae) for the tile encode AND decode. A
// re-detailed section is composited back into pixels that were generated and
@@ -1882,10 +1887,14 @@ function buildKrea2TileStage(graph, LG, place, opts) {
const vaeOpts = vae?.widgets?.find((x) => x.name === "vae_name")?.options?.values || [];
const vaeFile = vaeOpts.find((o) => /qwen.*image.*vae/i.test(o))
|| vaeOpts.find((o) => /wan.*2[._]?1.*vae/i.test(o) && !/upscale|2x/i.test(o));
const loraNames = (opts.realism && LG.createNode("LoraLoaderModelOnly")?.widgets?.find((x) => x.name === "lora_name")?.options?.values) || [];
const projLora = opts.realism && loraNames.find((o) => /krea2_turbo_projector_scale/i.test(o));
const realLora = opts.realism && loraNames.find((o) => /krea2-realism/i.test(o));
const posEnc = LG.createNode("CLIPTextEncode");
const negEnc = LG.createNode("CLIPTextEncode");
const usdu = LG.createNode("UltimateSDUpscaleNoUpscale");
if (!(unet && unetFile && clip && clipFile && vae && vaeFile && posEnc && negEnc && usdu)) return null;
if (opts.realism && (!projLora || !realLora)) { console.warn("[PromptChain][upscale] Realism mode needs Krea2-realism-V1 + krea2_turbo_projector_scale LoRAs installed"); return null; }
place(unet, 0, 0);
place(clip, 0, 180);
place(vae, 0, 360);
@@ -1902,7 +1911,23 @@ function buildKrea2TileStage(graph, LG, place, opts) {
clip.connect(0, posEnc, inputIdx(posEnc, "clip", 0));
clip.connect(0, negEnc, inputIdx(negEnc, "clip", 0));
opts.imageNode.connect(opts.imageSlot || 0, usdu, inputIdx(usdu, "upscaled_image", 0));
unet.connect(0, usdu, inputIdx(usdu, "model", 1));
let modelNode = unet;
const realismLoras = [];
if (opts.realism && projLora && realLora) {
const lora1 = LG.createNode("LoraLoaderModelOnly");
const lora2 = LG.createNode("LoraLoaderModelOnly");
place(lora1, 0, 540);
place(lora2, 300, 540);
setNamedWidget(lora1, "lora_name", projLora);
setNamedWidget(lora1, "strength_model", 0.5);
setNamedWidget(lora2, "lora_name", realLora);
setNamedWidget(lora2, "strength_model", 0.6);
modelNode.connect(0, lora1, inputIdx(lora1, "model", 0));
lora1.connect(0, lora2, inputIdx(lora2, "model", 0));
modelNode = lora2;
realismLoras.push(lora1.id, lora2.id);
}
modelNode.connect(0, usdu, inputIdx(usdu, "model", 1));
posEnc.connect(0, usdu, inputIdx(usdu, "positive", 2));
negEnc.connect(0, usdu, inputIdx(usdu, "negative", 3));
vae.connect(0, usdu, inputIdx(usdu, "vae", 4));
@@ -1921,7 +1946,7 @@ function buildKrea2TileStage(graph, LG, place, opts) {
if (typeof opts.seamFixDenoise === "number") setNamedWidget(usdu, "seam_fix_denoise", opts.seamFixDenoise);
setNamedWidget(usdu, "force_uniform_tiles", true);
setNamedWidget(usdu, "tiled_decode", false);
return { usdu, sideInputIds: [unet.id, clip.id, vae.id, posEnc.id, negEnc.id] };
return { usdu, sideInputIds: [unet.id, clip.id, vae.id, posEnc.id, negEnc.id, ...realismLoras] };
}
// The modal's tuning knobs in tile-stage shape — shared by every tile engine.
@@ -2096,6 +2121,7 @@ function buildKrea2EngineGraph(data, options = {}) {
imageNode: fit,
unetName: options.engineModel?.filename || null,
positiveText: (options.prompt || "").trim() || SDXL_TILE_NEUTRAL_POSITIVE,
realism: !!options.realism,
...tileStageOpts(options),
}));
}
+9
View File
@@ -8,6 +8,7 @@ import { ensureSvelteCSS, createModuleLoader } from "./lazy-load.js";
import { prepareUpscale, buildUpscaleWorkflow } from "./upscale-from-image.js";
import { upscaleImageInBackground, enlargeImageInBackground } from "./upscale-background.js";
import { prepareInpaint, runInpaint, saveInpaintResult, uploadInputImage, inpaintRegion, sourcePromptText, sourcePromptTextFull, liveWorkflowPrompt } from "./inpaint-background.js";
import { prepareI2I, runI2IInBackground } from "./i2i-background.js";
import { prepareEdit, saveEditedImage } from "./edit-image.js";
import { fetchReposeCaps } from "./repose-from-image.js";
import { runReposeInBackground } from "./repose-background.js";
@@ -292,6 +293,14 @@ export async function openViewer(images, startIndex = 0, workflowId = "", onDele
return prepared;
},
onInpaintRun: (prepared, options) => runInpaint(prepared, options),
onI2iPrepare: async (entry) => {
const prepared = await prepareI2I(entry);
if (prepared?.caps && prepared.data?.prompt) {
prepared.caps.referencePrompt = resolveReferencePrompt(prepared.data.prompt, workflowId);
}
return prepared;
},
onI2iRun: (prepared, options) => runI2IInBackground(prepared, options),
onInpaintSave: (doneState, prefix) => saveInpaintResult(doneState, prefix),
onInpaintUploadReference: (file) => uploadInputImage(file),
onInpaintInstallPack: (injectable) => installInpaintPack(injectable),
+1 -1
View File
@@ -148,7 +148,7 @@ let _lastPreviewUrl = null;
// generating-node map never includes the video node, so hideProgress() never
// fires and the preview stays stuck on "Generating…" after a video finishes.
const OUTPUT_NODE_TYPES = new Set(["SaveImage", "PreviewImage", "SaveVideo", "SaveWEBM"]);
const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale", "PromptChain_IdeogramSampler"]);
const KSAMPLER_TYPES = new Set(["KSampler", "KSamplerAdvanced", "PromptChain_RegionalDetailer", "SamplerCustomAdvanced", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale", "PromptChain_IdeogramSampler", "ClownsharKSampler_Beta"]);
const IMAGE_LATENT_TYPES = new Set(["IMAGE", "LATENT"]);
const LOAD_IMAGE_TYPES = new Set(["LoadImage"]);
+1 -1
View File
@@ -5,7 +5,7 @@
[project]
name = "comfyui-promptchain"
version = "0.1.6"
version = "0.1.7"
description = "Prompt-authoring and image-iteration suite for ComfyUI: structured prompts, 3D posing, model awareness, regional control, and layered editing."
license = { file = "LICENSE" }
requires-python = ">=3.10"
+62 -11
View File
@@ -25,6 +25,7 @@
onOpenInpaint = null, // ({compositeBlob, maskCanvas}) => void — opens the full Inpaint modal
onOpenUpscale = null, // ({cropBlob, rect}) => void — opens the Upscale modal on the region crop
onOpenRepose = null, // ({compositeBlob}) => void — opens the Re-pose modal on the whole composite
onOpenI2i = null, // ({compositeBlob, width, height}) => Promise<void> — opens the i2i modal on the /16-snapped composite
figureRegions = [], // [{name, text, maskUrl}] — the pose scene's region entities, for one-click figure selection
editDocHash = "", // content hash of the image being edited — restores its saved layer stack ("don't-flatten" persistence)
suspended = false, // an elevated modal (Inpaint/Upscale handoff) is open above — release the keyboard
@@ -46,7 +47,7 @@
let savingEdits = $state(false); // "Save edits" → opened image's sidecar, in flight
let editsSaved = $state(false); // "✓ Edits saved" chip; cleared by the next edit
let restoring = $state(false); // loading a saved layer stack on open
let handoffBusy = $state(null); // "inpaint" | "upscale" | "repose" while the handoff modal prepares (uploads/graph build)
let handoffBusy = $state(null); // "inpaint" | "upscale" | "repose" | "i2i" while the handoff modal prepares (uploads/graph build)
// Re-pose: hand the whole flattened composite to the Re-pose modal (pose a 3D
// figure → background re-render into the new pose). Whole-image, not a region.
@@ -60,6 +61,29 @@
finally { handoffBusy = null; }
}
// i2i: re-diffuse the whole flattened composite. The render dims are snapped
// DOWN to a multiple of 16 with a centered crop — that's exactly what the VAE
// encode would do server-side (it center-crops to /8, and DiT engines patchify
// ×2 = /16), but doing it here means the result comes back at known dims and
// composites over the original without the silent 1–3px shift a raw odd size
// would cause. addI2iLayerFromResult scales the result to fill the document.
async function openI2iHandoff() {
if (!onOpenI2i) return;
errorMsg = "";
flushActive();
const encW = Math.max(16, Math.floor(width / 16) * 16);
const encH = Math.max(16, Math.floor(height / 16) * 16);
const ox = (width - encW) >> 1, oy = (height - encH) >> 1;
const c = document.createElement("canvas");
c.width = encW; c.height = encH;
drawFlattened(c.getContext("2d"), ox, oy, encW, encH, 0, 0, encW, encH);
const compositeBlob = await new Promise((r) => c.toBlob(r, "image/png"));
handoffBusy = "i2i";
try { await onOpenI2i({ compositeBlob, width: encW, height: encH }); }
catch (e) { errorMsg = `Couldn't open i2i: ${e?.message || e}`; }
finally { handoffBusy = null; }
}
// ── view state (same stage-transform pattern as the inpaint painter) ──
let viewportEl;
let imgEl;
@@ -1329,6 +1353,17 @@
c.getContext("2d").drawImage(img, 0, 0, width, height);
insertResultLayer(c, finalMask || inpaintMaskUsed, "Inpaint");
}
// i2i hands back the full-frame re-diffusion — it covers the whole document,
// so it lands as an UNCLIPPED new layer scaled to fill (the result is the
// /16-snapped render, ≤15px smaller per axis, so the fill is sub-pixel).
// Exported for the viewer to call.
export async function addI2iLayerFromResult(url) {
const img = await loadImageEl(url);
const c = document.createElement("canvas");
c.width = width; c.height = height;
c.getContext("2d").drawImage(img, 0, 0, width, height);
insertResultLayer(c, null, "i2i");
}
// ── Region → Upscale ──
function maskBBox(maskCanvas) {
@@ -2738,6 +2773,7 @@
let strokeCov = null; // Float32Array width×height, current stroke coverage
let sourceData = null; // ImageData of the untouched source image
let preStroke = null; // paint-layer ImageData at stroke start
let selStrokeData = null; // active selection's alpha, snapshotted at stroke start (clips the brush to the selection SHAPE)
let batchBox = null; // dirty region of the current pointer batch
let strokeBox = null; // dirty region of the WHOLE stroke (heal resolves it at release)
let falloffLUT = { key: "", table: null };
@@ -2934,6 +2970,14 @@
// preStroke = the ACTIVE layer's current pixels (the live canvas), captured
// fresh so a layer switch can't leave it pointing at another layer's state.
preStroke = paintCanvas.getContext("2d").getImageData(0, 0, width, height);
// Snapshot the selection SHAPE (lasso curve / marquee / object-select) once
// per stroke so the brush clips to the true mask, not its bbox. null when
// nothing is selected → paint freely. renderStrokeBatch scales coverage by
// this alpha, and restores the pre-stroke pixel verbatim where cov ≤ 0, so
// neither brush nor eraser touch anything outside the selection.
selStrokeData = (selActive && selMaskCanvas)
? selMaskCanvas.getContext("2d").getImageData(0, 0, width, height).data
: null;
batchBox = null;
strokeBox = null;
}
@@ -3006,7 +3050,8 @@
const row = (y0 + y) * width + x0;
for (let x = 0; x < bw; x++) {
const gi = row + x;
const cov = Math.min(strokeCov[gi], ceil);
let cov = Math.min(strokeCov[gi], ceil);
if (selStrokeData) cov *= selStrokeData[gi * 4 + 3] / 255; // clip to the selection shape (feather-aware)
const i = (y * bw + x) * 4;
const j = gi * 4;
if (cov <= 0) {
@@ -4155,7 +4200,7 @@
<div class="pcr-ed-restoring">
<div class="pcr-ed-restoring-card">
<span class="pcr-ed-spinner"></span>
<span>{restoring ? "Restoring saved layers…" : handoffBusy === "upscale" ? "Preparing upscale…" : handoffBusy === "repose" ? "Preparing Re-pose…" : "Preparing inpaint…"}</span>
<span>{restoring ? "Restoring saved layers…" : handoffBusy === "upscale" ? "Preparing upscale…" : handoffBusy === "repose" ? "Preparing Re-pose…" : handoffBusy === "i2i" ? "Preparing i2i…" : "Preparing inpaint…"}</span>
</div>
</div>
{/if}
@@ -4600,9 +4645,12 @@
</div>
</div>
{#if onOpenInpaint || onOpenUpscale || onOpenRepose}
{#if onOpenInpaint || onOpenUpscale || onOpenRepose || onOpenI2i}
<div class="pcr-ed-panel">
<span class="pcr-ed-panel-title">Render {selActive ? "selection" : "layer"}</span>
<!-- 2×2 grid (flex-wrap, each button ~50%): Inpaint · i2i Blend /
Upscale · Re-pose. Absent callbacks drop their button and the
rest reflow. -->
<div class="pcr-ed-render-row">
{#if onOpenInpaint}
<button class="pcr-ed-ip-render" disabled={!!handoffBusy} onclick={openInpaintHandoff}
@@ -4610,19 +4658,21 @@
? "Inpaint the selected region — it's pre-masked; the result returns as a new layer."
: "Inpaint — an alpha-bounded layer pre-masks its silhouette, else paint the mask in the Inpaint editor; the result returns as a new layer."}>✦ Inpaint</button>
{/if}
{#if onOpenI2i}
<button class="pcr-ed-ip-render" disabled={!!handoffBusy} onclick={openI2iHandoff}
title="i2i Blend — re-diffuse the whole image at the denoise you pick (pick an engine, type a prompt). The result returns as a new layer.">✦ i2i Blend</button>
{/if}
{#if onOpenUpscale}
<button class="pcr-ed-ip-render" disabled={!!handoffBusy} onclick={openUpscaleHandoff}
title={selActive
? "Upscale the selected region at the quality you pick."
: "Upscale the active layer's content at the quality you pick — grow the canvas to keep it 1:1, or squeeze it back in place."}>✦ Upscale</button>
{/if}
</div>
{#if onOpenRepose}
<div class="pcr-ed-render-row">
{#if onOpenRepose}
<button class="pcr-ed-ip-render" disabled={!!handoffBusy} onclick={openReposeHandoff}
title="Re-pose — pose a 3D figure and re-render this whole image into the new pose. Result is a new image in lineage.">✦ Re-pose</button>
</div>
{/if}
{/if}
</div>
</div>
{/if}
@@ -5076,8 +5126,9 @@
}
.pcr-ed-ip-prompt:focus { border-color: #c85909; }
.pcr-ed-ip-row { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
.pcr-ed-render-row { display: flex; gap: 8px; }
.pcr-ed-render-row .pcr-ed-ip-render { flex: 1; margin-top: 0; width: auto; white-space: nowrap; }
.pcr-ed-render-row { display: flex; flex-wrap: wrap; gap: 8px; }
/* ~50% basis → two buttons per row (a lone trailing button grows to full). */
.pcr-ed-render-row .pcr-ed-ip-render { flex: 1 1 calc(50% - 4px); margin-top: 0; width: auto; white-space: nowrap; }
.pcr-ed-ip-render {
width: 100%; margin-top: 6px; padding: 7px 10px; font-size: 12.5px; color: #fff; cursor: pointer;
background: #c85909; border: none; border-radius: 5px;
+648
View File
@@ -0,0 +1,648 @@
<script>
// I2IModal — full-frame image-to-image. A [Source] | [Output] viewport over
// the editor composite (no mask, no painting), a PromptChain-style prompt, an
// engine + denoise. Apply renders off-screen (auto-switching to Output,
// repeatable); Add to Edit hands the result back as a new layer.
//
// A no-mask sibling of InpaintModal: same engine picker / prompt editor /
// settings, minus the mask painter, the Regional/Depth modes, and the
// conditions panel (all mask-spatial).
import { untrack } from "svelte";
import { portal } from "../lib/portal.js";
import { recallModalMemory, storeModalMemory } from "../lib/modal-memory.js";
import SettingsSlider from "./model/SettingsSlider.svelte";
import SearchableSelect from "./shared/SearchableSelect.svelte";
import "./sidebar/modal-shared.css";
let {
open,
sourceUrl = "",
width = 0,
height = 0,
filename = "",
caps = null,
prefillPrompt = "",
referencePrompt = "",
apiURL = (p) => p,
onRun = null, // (options) => tracker
onUseInEdit = null, // (doneState) => void — hand the render back to Edit as a layer
elevated = false, // raise z-index above the Edit modal
mountPromptEditor = null, // (container, initialValue, onChange, modelInfo) => Promise<EditorView>
imageKey = "", // stable id of the image (per-image session memory)
onCancel,
} = $props();
let activeTab = $state("source");
let prompt = $state("");
let denoise = $state(0.55);
let sampler = $state("");
let scheduler = $state("");
let steps = $state(0); // engine recipe on open; 0 = inherit (source model)
let cfg = $state(0); // engine recipe on open; 0 = inherit (source model)
let advancedOpen = $state(false);
let promptEditorView = null;
// Engine: "source" = re-diffuse with the image's own model; a picked hash =
// a standalone graph on that checkpoint/UNET. P0 roster: source + SDXL /
// FLUX.1 / Krea 2 (prepareI2I drops Qwen Edit).
let engineSel = $state("source");
let engineEntry = $derived(caps?.engineModels?.find((m) => m.hash === engineSel) || null);
let engineKind = $derived(engineEntry
? (engineEntry.architecture === "flux" ? "flux1" : engineEntry.architecture === "krea2" ? "krea2" : "sdxl")
: "source");
// Realism mode: a krea2_turbo-only recipe (abliterated encoder + wan_2.1 VAE +
// realism/bypass LoRAs + the ClownsharKSampler tail — full-frame, so safe).
let realism = $state(false);
let realismAvailable = $derived(engineKind === "krea2" && /turbo/i.test(engineEntry?.filename || ""));
let engineGroups = $derived({
sdxl: (caps?.engineModels || []).filter((m) => m.architecture === "sdxl"),
flux1: (caps?.engineModels || []).filter((m) => m.architecture === "flux"),
krea2: (caps?.engineModels || []).filter((m) => m.architecture === "krea2"),
});
let engineSelectGroups = $derived([
...(caps?.sourceUsable !== false
? [{ label: "", options: [{ value: "source", label: "Source model — this image's own workflow (default)" }] }]
: []),
...(engineGroups.sdxl.length
? [{ label: "SDXL — re-diffuse with this checkpoint", options: engineGroups.sdxl.map((m) => ({ value: m.hash, label: m.displayName })) }]
: []),
...(engineGroups.flux1.length
? [{ label: "FLUX.1 — re-diffuse with this model", options: engineGroups.flux1.map((m) => ({ value: m.hash, label: m.displayName })) }]
: []),
...(engineGroups.krea2.length
? [{ label: "Krea 2 — re-diffuse with this model", options: engineGroups.krea2.map((m) => ({ value: m.hash, label: m.displayName })) }]
: []),
]);
// Per-engine recipe — read off caps (stamped by prepareI2I from the single
// i2iRecipe source of truth). No table lives here. The source model has no
// fixed recipe: 0/"" inherit the image's own gen (apply() passes undefined so
// the builder falls through to the source's embedded steps/cfg/sampler).
function engineRecipe(entry) {
if (!entry) return { steps: 0, cfg: 0, denoise: caps?.sourceI2iRecipe?.denoise ?? caps?.defaultDenoise ?? 0.55, sampler: "", scheduler: "" };
const r = entry.i2i || {};
return { steps: r.steps ?? 0, cfg: r.cfg ?? 0, denoise: r.denoise ?? caps?.defaultDenoise ?? 0.55, sampler: r.sampler || "", scheduler: r.scheduler || "" };
}
let recSampler = $derived(engineEntry ? engineRecipe(engineEntry).sampler : caps?.recommendedSampler);
let recScheduler = $derived(engineEntry ? engineRecipe(engineEntry).scheduler : caps?.recommendedScheduler);
let recSteps = $derived(engineRecipe(engineEntry).steps);
let recCfg = $derived(engineRecipe(engineEntry).cfg);
function pickEngine(value) {
engineSel = value;
const entry = caps?.engineModels?.find((m) => m.hash === value) || null;
const r = engineRecipe(entry);
if (entry) { sampler = r.sampler; scheduler = r.scheduler; }
else { sampler = caps?.defaultSampler || ""; scheduler = caps?.defaultScheduler || ""; }
steps = r.steps; cfg = r.cfg; denoise = r.denoise;
}
// Model identity for the prompt editor's templates/autocomplete: the picked
// engine model, else the image's own source model.
function editorModelInfo() {
if (engineEntry) return { hash: engineEntry.hash, architecture: engineEntry.architecture };
return caps?.sourceModelInfo || null;
}
let memoryPrompt = null;
function prefillFor() {
if (typeof memoryPrompt === "string" && memoryPrompt.trim()) return memoryPrompt;
return prefillPrompt || "";
}
let progress = $state(null);
let doneState = $state(null);
let errorMsg = $state("");
let tracker = null;
let unsub = null;
let wasOpen = false;
$effect(() => {
if (open && caps && !wasOpen) {
wasOpen = true;
untrack(() => {
activeTab = "source";
const mem = recallModalMemory("i2i", imageKey);
const memEngineValid = mem?.engine === "source"
? caps.sourceUsable !== false
: !!mem?.engine && !!caps.engineModels?.some((m) => m.hash === mem.engine);
engineSel = memEngineValid ? mem.engine
: caps.sourceUsable === false ? (caps.engineModels?.[0]?.hash || "source") : "source";
memoryPrompt = (mem && typeof mem.prompt === "string" && mem.prompt.trim()) ? mem.prompt : null;
prompt = prefillFor();
const entry = caps.engineModels?.find((m) => m.hash === engineSel) || null;
const r = engineRecipe(entry);
if (entry) { sampler = r.sampler; scheduler = r.scheduler; }
else { sampler = caps.defaultSampler || ""; scheduler = caps.defaultScheduler || ""; }
steps = r.steps; cfg = r.cfg; denoise = r.denoise;
if (memEngineValid && typeof mem.denoise === "number") denoise = mem.denoise;
advancedOpen = false;
progress = null;
doneState = null;
errorMsg = "";
requestAnimationFrame(() => { resetView(); });
});
} else if (!open) {
wasOpen = false;
}
});
// Ctrl+Enter applies; never let it bubble to ComfyUI's Queue Prompt.
$effect(() => {
if (!open) return;
const onKey = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
e.stopImmediatePropagation();
apply();
}
};
window.addEventListener("keydown", onKey, true);
return () => window.removeEventListener("keydown", onKey, true);
});
let running = $derived(!!progress && progress.phase !== "done" && progress.phase !== "error" && progress.phase !== "cancelled");
// Nothing to render with: the image has no graftable source AND no engine
// model is installed. Disable Apply with a reason instead of letting it throw.
let noEngine = $derived(caps?.sourceUsable === false && !(caps?.engineModels?.length));
let outputUrl = $derived((() => {
if (progress?.phase === "running" && progress.previewUrl) return progress.previewUrl;
if (doneState?.temp) {
const t = doneState.temp;
return apiURL(`/view?filename=${encodeURIComponent(t.filename)}&subfolder=${encodeURIComponent(t.subfolder || "")}&type=${t.type || "temp"}&rand=${doneState.rand}`);
}
return null;
})());
let progressPercent = $derived(
progress?.phase === "running" && progress.max ? Math.round((progress.value / progress.max) * 100) : null
);
// ── viewport (pan/zoom, no painting) ──
let viewportEl;
let zoom = $state(1);
let panX = $state(0);
let panY = $state(0);
let panning = false;
let lastPt = null;
let compareSplit = $state(false);
let splitX = $state(0);
let splitDragging = false;
function resetView() {
if (!viewportEl || !width || !height) { zoom = 1; panX = 0; panY = 0; return; }
const rect = viewportEl.getBoundingClientRect();
zoom = Math.min(rect.width / width, rect.height / height, 1) || 1;
panX = (rect.width - width * zoom) / 2;
panY = (rect.height - height * zoom) / 2;
}
function onPointerDown(e) {
if (e.button > 2) return;
panning = true;
lastPt = { x: e.clientX, y: e.clientY };
e.preventDefault();
viewportEl.setPointerCapture(e.pointerId);
}
function onPointerMove(e) {
if (!panning) return;
panX += e.clientX - lastPt.x;
panY += e.clientY - lastPt.y;
lastPt = { x: e.clientX, y: e.clientY };
}
function onPointerUp() { panning = false; lastPt = null; }
function onWheel(e) {
e.preventDefault();
const rect = viewportEl.getBoundingClientRect();
const cx = e.clientX - rect.left, cy = e.clientY - rect.top;
const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
const newZoom = Math.max(0.05, Math.min(12, zoom * factor));
panX = cx - ((cx - panX) / zoom) * newZoom;
panY = cy - ((cy - panY) / zoom) * newZoom;
zoom = newZoom;
}
function toggleCompare() {
if (!outputUrl || running) return;
compareSplit = !compareSplit;
if (compareSplit) {
const rect = viewportEl?.getBoundingClientRect();
splitX = rect ? rect.width / 2 : 0;
}
}
function onSplitDown(e) {
e.stopPropagation(); e.preventDefault();
splitDragging = true;
e.currentTarget.setPointerCapture(e.pointerId);
}
function onSplitMove(e) {
if (!splitDragging) return;
e.stopPropagation();
const rect = viewportEl.getBoundingClientRect();
splitX = Math.max(0, Math.min(rect.width, e.clientX - rect.left));
}
function onSplitUp(e) {
splitDragging = false;
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* already released */ }
}
// Prompt editor — remounts on engine flip so the seed re-applies.
let promptSeedKey = $derived(engineKind);
function promptEditor(node) {
let disposed = false;
let view = null;
(async () => {
if (!mountPromptEditor) return;
const v = await mountPromptEditor(node, untrack(() => prefillFor()), (text) => { prompt = text; }, editorModelInfo);
if (disposed) { v?.destroy?.(); return; }
view = v;
promptEditorView = v;
})().catch((e) => console.error("[i2i] prompt editor mount failed", e));
return {
destroy() {
disposed = true;
view?.destroy?.();
if (promptEditorView === view) promptEditorView = null;
},
};
}
// ── actions ──
function apply() {
if (!onRun || running) return;
doneState = null;
errorMsg = "";
activeTab = "output";
tracker = onRun({
prompt, denoise,
sampler: sampler || null, scheduler: scheduler || null,
steps: steps > 0 ? steps : undefined,
cfg: cfg > 0 ? cfg : undefined,
engine: engineKind === "sdxl" ? "sdxl-ckpt"
: engineKind === "flux1" ? "flux1-unet"
: engineKind === "krea2" ? "krea2-unet" : "source",
engineModel: engineEntry ? { hash: engineEntry.hash, filename: engineEntry.filename } : undefined,
engineGen: engineEntry?.gen || undefined,
realism: realismAvailable ? realism : undefined,
});
storeModalMemory("i2i", imageKey, {
prompt,
engine: engineKind === "source" ? "source" : engineSel,
denoise,
});
unsub?.();
unsub = tracker.subscribe((state) => {
progress = state;
if (state.phase === "done") {
doneState = { ...state, rand: Math.random() };
progress = null;
} else if (state.phase === "cancelled") {
progress = null;
activeTab = "source";
} else if (state.phase === "error") {
errorMsg = state.message || "i2i failed";
progress = null;
activeTab = "source";
}
});
}
function stopRun() { tracker?.cancel?.(); }
function close() {
unsub?.();
unsub = null;
tracker = null;
onCancel?.();
}
function handleKeydown(e) {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
if (running) return;
close();
}
}
</script>
{#if open}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div use:portal class="pcr-modal-backdrop" style:z-index={elevated ? 10006 : null} onkeydown={handleKeydown}>
<div class="pcr-modal pcr-ip-modal" role="dialog" aria-modal="true">
<div class="pcr-modal-header">
<span class="pcr-modal-title">{running ? "i2i Blend…" : "i2i Blend"}{filename ? ` — ${filename}` : ""}{width && height ? ` · ${width}×${height}` : ""}</span>
{#if !running}
<button class="pcr-modal-close" onclick={close} aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
{/if}
</div>
<div class="pcr-modal-body pcr-ip-body">
<div class="pcr-ip-left">
<div class="pcr-ip-tabs">
<div class="pcr-ip-seg">
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="pcr-ip-tab" class:active={activeTab === "source"} onclick={() => { if (!running) activeTab = "source"; }}>Source</div>
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="pcr-ip-tab" class:active={activeTab === "output"} onclick={() => { activeTab = "output"; }}>Output</div>
</div>
<div class="pcr-ip-tools">
<button class="pcr-ip-tool-btn" onclick={resetView}>Fit</button>
{#if activeTab === "output"}
<button class="pcr-ip-tool-btn" class:on={compareSplit}
disabled={!outputUrl || running} onclick={toggleCompare}
title="Drag the divider to wipe the source over the result">Compare</button>
{/if}
</div>
</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="pcr-ip-viewport out-mode"
bind:this={viewportEl}
onpointerdown={onPointerDown}
onpointermove={onPointerMove}
onpointerup={onPointerUp}
onpointercancel={onPointerUp}
onwheel={onWheel}
oncontextmenu={(e) => e.preventDefault()}
ondragstart={(e) => e.preventDefault()}
>
{#if activeTab === "source"}
<div class="pcr-ip-stage" style="transform: translate({panX}px, {panY}px) scale({zoom});">
<img src={sourceUrl} alt="" draggable="false" width={width} height={height} />
</div>
{:else}
{#if running && progress?.previewUrl}
<div class="pcr-ip-center">
<img class="pcr-ip-live-preview" src={progress.previewUrl} alt="" draggable="false" />
</div>
{:else if outputUrl}
<div class="pcr-ip-stage" style="transform: translate({panX}px, {panY}px) scale({zoom});">
<img src={outputUrl} alt="" draggable="false" width={width} height={height} />
</div>
{#if compareSplit}
<div class="pcr-ip-split-before" style="clip-path: inset(0 calc(100% - {splitX}px) 0 0);">
<div class="pcr-ip-stage" style="transform: translate({panX}px, {panY}px) scale({zoom});">
<img src={sourceUrl} alt="" draggable="false" width={width} height={height} />
</div>
</div>
<div class="pcr-ip-split-label before">Source</div>
<div class="pcr-ip-split-label after">Result</div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="pcr-ip-split-divider" style="left: {splitX}px;"
onpointerdown={onSplitDown} onpointermove={onSplitMove}
onpointerup={onSplitUp} onpointercancel={onSplitUp}>
<div class="pcr-ip-split-knob">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9.5 8 5.5 12 9.5 16"/><polyline points="14.5 8 18.5 12 14.5 16"/>
</svg>
</div>
</div>
{/if}
{:else}
<div class="pcr-ip-center">
<div class="pcr-ip-waiting">{running ? "Waiting for the sampler…" : "No output yet — set a denoise and Apply."}</div>
</div>
{/if}
{#if running}
<div class="pcr-ip-progress">
<div class="pcr-ip-bar">
<div class="pcr-ip-bar-fill" class:indeterminate={progressPercent == null}
style={progressPercent != null ? `width: ${progressPercent}%` : ""}></div>
</div>
{#if progressPercent != null}
<span class="pcr-ip-bar-label">step {progress.value}/{progress.max}</span>
{/if}
</div>
{/if}
{/if}
</div>
</div>
<div class="pcr-ip-right">
{#if caps?.engineModels?.length}
<div class="pcr-mcard">
<div class="pcr-mcard-title">Engine</div>
<SearchableSelect
id="pcr-i2i-engine"
value={engineSel}
groups={engineSelectGroups}
popupKey="i2i-engine"
disabled={running}
onpick={(v) => pickEngine(v)}
/>
{#if engineKind === "source"}
<div class="pcr-ip-hint">re-diffuses the whole image with its own model at the denoise below</div>
{:else if engineKind === "sdxl"}
<div class="pcr-ip-hint">re-diffuses the whole image with this SDXL checkpoint — describe the scene</div>
{:else if engineKind === "flux1"}
<div class="pcr-ip-hint">re-diffuses the whole image with this FLUX.1 model — strong on faces and eyes</div>
{:else if engineKind === "krea2"}
<div class="pcr-ip-hint">re-diffuses the whole image with this Krea 2 model — sharp detail, clean structure</div>
{/if}
{#if realismAvailable}
<label class="pcr-ip-hint" style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:6px;">
<input type="checkbox" bind:checked={realism} />
Realism — abliterated encoder + wan VAE + realism/bypass LoRAs (NSFW-capable)
</label>
{/if}
</div>
{/if}
<div class="pcr-mcard">
<div class="pcr-mcard-title">Prompt</div>
{#key promptSeedKey}
{#if mountPromptEditor}
<div class="pcr-ip-prompt pcr-ip-prompt-editor" use:promptEditor></div>
{:else}
<textarea
class="pcr-ip-prompt"
bind:value={prompt}
spellcheck="false"
placeholder={"what the whole image should become\n\nNegative Prompt:\noptional"}
></textarea>
{/if}
{/key}
{#if referencePrompt}
<span class="pcr-ip-label">Workflow prompt (reference)</span>
<textarea class="pcr-ip-refprompt" readonly spellcheck="false">{referencePrompt}</textarea>
{/if}
</div>
<div class="pcr-mcard">
<div class="pcr-mcard-title">Settings</div>
<div class="pcr-ip-denoise">
<span class="pcr-ip-label">Denoise</span>
<SettingsSlider min={0.05} max={1} step={0.01} value={denoise}
savedValue={engineRecipe(engineEntry).denoise} onChange={(v) => { denoise = v; }} />
</div>
{#if caps?.samplerOptions?.length}
<div class="pcr-ip-combos">
<div class="pcr-ip-combo">
<label class="pcr-ip-label" for="pcr-i2i-sampler">Sampler</label>
<select id="pcr-i2i-sampler" class="pcr-ip-select" class:at-rec={sampler === recSampler} bind:value={sampler}>
{#each caps.samplerOptions as opt}
<option value={opt} style:color={opt === recSampler ? "#5ed357" : "#999"}>{opt}{opt === recSampler ? " ●" : ""}</option>
{/each}
</select>
</div>
<div class="pcr-ip-combo">
<label class="pcr-ip-label" for="pcr-i2i-scheduler">Scheduler</label>
<select id="pcr-i2i-scheduler" class="pcr-ip-select" class:at-rec={scheduler === recScheduler} bind:value={scheduler}>
{#each caps.schedulerOptions as opt}
<option value={opt} style:color={opt === recScheduler ? "#5ed357" : "#999"}>{opt}{opt === recScheduler ? " ●" : ""}</option>
{/each}
</select>
</div>
</div>
{/if}
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div class="pcr-ip-adv-head" onclick={() => { advancedOpen = !advancedOpen; }}>
{advancedOpen ? "▾" : "▸"} Advanced
</div>
{#if advancedOpen}
<div class="pcr-ip-denoise">
<span class="pcr-ip-label">Steps</span>
<SettingsSlider min={0} max={60} step={1} value={steps}
savedValue={recSteps} onChange={(v) => { steps = v; }} />
</div>
<div class="pcr-ip-denoise">
<span class="pcr-ip-label">CFG</span>
<SettingsSlider min={0} max={10} step={0.1} value={cfg}
savedValue={recCfg} onChange={(v) => { cfg = v; }} />
</div>
<div style="opacity:0.55;font-size:11px;padding:2px 0 0 2px">On the source model, 0 = inherit the image's own steps/cfg.</div>
{/if}
</div>
</div>
</div>
{#if errorMsg}
<div class="pcr-ip-error">⚠ {errorMsg}</div>
{/if}
<div class="pcr-modal-footer">
{#if running}
<button class="pcr-modal-btn pcr-modal-btn-danger" onclick={stopRun}>Stop</button>
{:else}
<button class="pcr-modal-btn pcr-modal-btn-secondary" onclick={close}>Cancel</button>
<button class="pcr-modal-btn pcr-modal-btn-primary" onclick={apply}
disabled={noEngine}
title={noEngine ? "This image has no render data to re-diffuse and no compatible engine model is installed." : ""}>
{doneState ? "Re-Apply" : "Apply"}
</button>
{#if onUseInEdit}
<button class="pcr-modal-btn pcr-modal-btn-primary" onclick={() => onUseInEdit(doneState)} disabled={!doneState}>
Add to Edit
</button>
{/if}
{/if}
</div>
</div>
</div>
{/if}
<style>
.pcr-ip-modal {
width: 96vw; height: 94vh;
min-width: 900px; max-width: 96vw;
display: flex; flex-direction: column;
}
.pcr-ip-body { display: flex; gap: 16px; flex: 1; min-height: 0; }
.pcr-ip-left { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.pcr-ip-right { flex: 0 0 320px; min-height: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 4px; }
.pcr-ip-tabs { display: flex; align-items: center; margin-bottom: 8px; }
.pcr-ip-seg { display: flex; border: 1px solid #3a3a3a; border-radius: 5px; overflow: hidden; }
.pcr-ip-tab { padding: 4px 18px; font-size: 12px; color: #aaa; cursor: pointer; background: transparent; }
.pcr-ip-tab + .pcr-ip-tab { border-left: 1px solid #3a3a3a; }
.pcr-ip-tab:hover:not(.active) { color: #ddd; }
.pcr-ip-tab.active { background: #c85909; color: #fff; }
.pcr-ip-tools { display: flex; align-items: center; gap: 8px; margin-left: auto; align-self: center; }
.pcr-ip-tool-btn {
padding: 3px 10px; font-size: 11.5px; color: #aaa;
background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px; cursor: pointer;
}
.pcr-ip-tool-btn:hover { color: #fff; border-color: #555; }
.pcr-ip-tool-btn.on { color: #fff; border-color: #c85909; background: rgba(200, 89, 9, 0.2); }
.pcr-ip-tool-btn:disabled { opacity: 0.4; cursor: default; }
.pcr-ip-viewport {
position: relative; overflow: hidden;
flex: 1; min-height: 0; background: #141414;
border: 1px solid #3a3a3a; border-radius: 6px;
touch-action: none; cursor: grab;
}
.pcr-ip-viewport:active { cursor: grabbing; }
.pcr-ip-stage { position: absolute; transform-origin: 0 0; }
.pcr-ip-stage img { display: block; user-select: none; pointer-events: none; }
.pcr-ip-split-before { position: absolute; inset: 0; z-index: 4; pointer-events: none; }
.pcr-ip-split-divider {
position: absolute; top: 0; bottom: 0;
width: 18px; margin-left: -9px;
z-index: 7; cursor: ew-resize; touch-action: none;
}
.pcr-ip-split-divider::before {
content: ""; position: absolute; top: 0; bottom: 0; left: 50%;
width: 2px; margin-left: -1px;
background: rgba(255, 255, 255, 0.9); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.pcr-ip-split-knob {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 30px; height: 30px; border-radius: 50%;
background: rgba(20, 20, 20, 0.85); border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center; color: #fff;
}
.pcr-ip-split-knob svg { width: 18px; height: 18px; }
.pcr-ip-split-label {
position: absolute; top: 8px; z-index: 6;
padding: 2px 7px; font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
color: #fff; background: rgba(20, 20, 20, 0.7); border-radius: 4px;
pointer-events: none;
}
.pcr-ip-split-label.before { left: 8px; }
.pcr-ip-split-label.after { right: 8px; }
.pcr-ip-center { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.pcr-ip-live-preview { max-width: 100%; max-height: 100%; object-fit: contain; }
.pcr-ip-waiting { font-size: 12.5px; color: #777; }
.pcr-ip-combos { display: flex; gap: 10px; }
.pcr-ip-combo { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.pcr-ip-select {
width: 100%; box-sizing: border-box;
padding: 5px 8px; font-size: 12px; color: #999;
background: #1c1c1c; border: 1px solid #3a3a3a; border-radius: 5px;
outline: none; cursor: pointer;
}
.pcr-ip-select.at-rec { color: #5ed357; border-color: rgba(94, 211, 87, 0.45); }
.pcr-ip-progress { position: absolute; left: 16px; right: 16px; bottom: 12px; display: flex; align-items: center; gap: 10px; }
.pcr-ip-bar { flex: 1; height: 6px; border-radius: 3px; background: rgba(0, 0, 0, 0.55); border: 1px solid #3a3a3a; overflow: hidden; }
.pcr-ip-bar-fill { height: 100%; background: #c85909; transition: width 0.2s ease; }
.pcr-ip-bar-fill.indeterminate { width: 35%; animation: pcr-i2i-slide 1.2s ease-in-out infinite alternate; }
@keyframes pcr-i2i-slide { from { margin-left: 0; } to { margin-left: 65%; } }
.pcr-ip-bar-label { font-size: 10.5px; color: #bbb; }
.pcr-ip-label { font-size: 11px; color: #888; margin-top: 6px; }
.pcr-ip-adv-head { margin-top: 8px; font-size: 11px; color: #888; cursor: pointer; user-select: none; }
.pcr-ip-adv-head:hover { color: #ccc; }
.pcr-ip-hint { font-size: 11px; color: #777; }
.pcr-ip-prompt {
min-height: 200px; resize: vertical;
padding: 8px 10px; font-size: 12.5px; line-height: 1.45; color: #ddd;
background: #1c1c1c; border: 1px solid #3a3a3a; border-radius: 5px;
outline: none; font-family: inherit;
}
.pcr-ip-prompt:focus { border-color: #c85909; }
.pcr-ip-prompt-editor {
height: 220px; padding: 0; resize: none; overflow: hidden;
display: flex; flex-direction: column;
}
.pcr-ip-prompt-editor:focus-within { border-color: #c85909; }
.pcr-ip-prompt-editor :global(.cm-editor) { flex: 1; min-height: 0; height: 100%; }
.pcr-ip-prompt-editor :global(.cm-scroller) { overflow: auto; }
.pcr-ip-refprompt {
height: 110px; resize: vertical;
padding: 7px 10px; font-size: 11.5px; line-height: 1.4; color: #9a9a9a;
background: #161616; border: 1px solid #313131; border-radius: 5px;
outline: none; font-family: inherit; cursor: text; user-select: text;
}
.pcr-ip-denoise { display: flex; align-items: center; gap: 10px; }
.pcr-ip-denoise .pcr-ip-label { flex: none; width: 52px; margin-top: 0; }
.pcr-modal-btn:disabled { opacity: 0.45; cursor: default; }
.pcr-ip-error {
margin: 0 2px 2px; padding: 7px 12px;
font-size: 12px; color: #ff8a80; line-height: 1.4;
background: rgba(229, 57, 53, 0.12); border: 1px solid rgba(229, 57, 53, 0.4);
border-radius: 5px; word-break: break-word;
}
</style>
+66 -1
View File
@@ -2,6 +2,7 @@
import { onDestroy } from "svelte";
import UpscaleOptionsModal from "./UpscaleOptionsModal.svelte";
import InpaintModal from "./InpaintModal.svelte";
import I2IModal from "./I2IModal.svelte";
import EditModal from "./EditModal.svelte";
import RePoseModal from "./RePoseModal.svelte";
import ExtendModal from "./ExtendModal.svelte";
@@ -25,6 +26,8 @@
onUpscaleEnlarge = null,
onInpaintPrepare = null,
onInpaintRun = null,
onI2iPrepare = null, // (entry) => Promise<{data, caps}> — i2i caps (inpaint caps, no mask)
onI2iRun = null, // (prepared, options) => tracker — background full-frame i2i
onInpaintSave = null,
onInpaintUploadReference = null,
onInpaintInstallPack = null,
@@ -1517,6 +1520,11 @@
let inpaintEditMask = $state(null); // the region mask handed from Edit
let inpaintFromEdit = $state(false);
// Edit → i2i handoff state (full-frame re-diffusion, no mask).
let i2iModalOpen = $state(false);
let i2iPrepared = $state(null);
let i2iFromEdit = $state(false);
async function openEdit(targetHash = displayedHash) {
if (editPreparing || !onEditPrepare || !onEditSave) return;
if (isVideoHash(targetHash)) return; // the editor can't open a video clip
@@ -1617,6 +1625,42 @@
closeInpaintFromEdit();
}
// Edit → i2i handoff: open the i2i modal on the edited composite (already
// /16-snapped by EditModal.openI2iHandoff); its result returns to Edit as a
// new full-frame layer. Mirrors openInpaintFromEdit, minus the mask.
async function openI2iFromEdit({ compositeBlob, width: encW, height: encH }) {
if (!onI2iPrepare || !onI2iRun || !editEntry) return;
try {
const prepared = await onI2iPrepare(editEntry);
if (!prepared) return;
// Upload the composite as the render input (reuse the inpaint uploader —
// same content-addressed input/promptchain_inpaint scoping).
const name = onInpaintUploadReference
? await onInpaintUploadReference(new File([compositeBlob], "edit-composite.png", { type: "image/png" }))
: null;
i2iFromEdit = true;
i2iPrepared = {
...prepared,
data: { ...prepared.data, input_ref: name || prepared.data?.input_ref, width: encW, height: encH },
sourceUrl: URL.createObjectURL(compositeBlob),
};
i2iModalOpen = true;
} catch (e) {
console.error("[PromptChain] i2i-from-edit failed", e);
}
}
function closeI2iFromEdit() {
i2iModalOpen = false; i2iFromEdit = false; i2iPrepared = null;
}
async function handleI2iToEdit(doneState) {
const t = doneState?.temp;
if (t && editModalRef?.addI2iLayerFromResult) {
const url = apiURL(`/view?filename=${encodeURIComponent(t.filename)}&subfolder=${encodeURIComponent(t.subfolder || "")}&type=${t.type || "temp"}&rand=${Math.random()}`);
try { await editModalRef.addI2iLayerFromResult(url); } catch (e) { console.error("[PromptChain] add i2i layer failed", e); }
}
closeI2iFromEdit();
}
// Edit → Upscale handoff: the upscale pipeline runs on the region crop, and
// the render returns to Edit either 1:1 in a GROWN canvas (the document
// scales to the quality the user picked) or squeezed back into the region —
@@ -2950,6 +2994,26 @@
onCancel={() => { if (inpaintFromEdit) closeInpaintFromEdit(); else { inpaintModalOpen = false; inpaintPrepared = null; } }}
/>
<I2IModal
open={i2iModalOpen}
imageKey={displayedHash || ""}
sourceUrl={i2iPrepared?.sourceUrl || ""}
width={i2iPrepared?.data?.width || 0}
height={i2iPrepared?.data?.height || 0}
filename={i2iPrepared?.data?.filename || ""}
caps={i2iPrepared?.caps}
prefillPrompt={i2iPrepared?.caps?.prefillPrompt
|| (imageInfo?.prompt ? imageInfo.prompt + (imageInfo.negative ? `\n\nNegative Prompt:\n${imageInfo.negative}` : "") : "")}
referencePrompt={i2iPrepared?.caps?.referencePrompt
|| (imageInfo?.prompt ? imageInfo.prompt + (imageInfo.negative ? `\n\nNegative Prompt:\n${imageInfo.negative}` : "") : "")}
{apiURL}
onRun={(options) => onI2iRun(i2iPrepared, options)}
mountPromptEditor={onMountPromptEditor}
onUseInEdit={handleI2iToEdit}
elevated={true}
onCancel={closeI2iFromEdit}
/>
<EditModal
bind:this={editModalRef}
open={editModalOpen}
@@ -2966,8 +3030,9 @@
onOpenInpaint={onInpaintRegion ? openInpaintFromEdit : null}
onOpenUpscale={onUpscalePrepare && onUpscaleBackground && onInpaintUploadReference ? openUpscaleFromEdit : null}
onOpenRepose={onReposeCaps && onReposeRun && onMountPoser && onInpaintUploadReference ? openReposeFromEdit : null}
onOpenI2i={onI2iPrepare && onI2iRun && onInpaintUploadReference ? openI2iFromEdit : null}
figureRegions={editFigureRegions}
suspended={(inpaintFromEdit && inpaintModalOpen) || (upscaleFromEdit && upscaleModalOpen) || reposeModalOpen}
suspended={(inpaintFromEdit && inpaintModalOpen) || (upscaleFromEdit && upscaleModalOpen) || reposeModalOpen || i2iModalOpen}
onCancel={() => { editModalOpen = false; editPrepared = null; editEntry = null; }}
/>
+11
View File
@@ -80,6 +80,10 @@
let engineKind = $derived(engineEntry
? (engineEntry.architecture === "qwen_edit" ? "qwen" : engineEntry.architecture === "flux" ? "flux1" : engineEntry.architecture === "krea2" ? "krea2" : "sdxl")
: "source");
// Realism mode: a krea2_turbo-only recipe (abliterated encoder + realism/bypass
// LoRAs). Inpaint keeps qwen_image_vae (seam-safe) and MaskedDetail's sampler.
let realism = $state(false);
let realismAvailable = $derived(engineKind === "krea2" && /turbo/i.test(engineEntry?.filename || ""));
let engineGroups = $derived({
sdxl: (caps?.engineModels || []).filter((m) => m.architecture === "sdxl"),
flux1: (caps?.engineModels || []).filter((m) => m.architecture === "flux"),
@@ -792,6 +796,7 @@
: engineKind === "qwen" ? "qwen-edit" : "source",
engineModel: engineEntry ? { hash: engineEntry.hash, filename: engineEntry.filename } : undefined,
engineGen: engineEntry?.gen || undefined,
realism: realismAvailable ? realism : undefined,
// The qwen builder crops around the mask client-side — the bbox is
// known here (the painted canvas), not on the server.
maskBbox: engineKind === "qwen" ? maskBboxFromCanvas() : undefined,
@@ -1052,6 +1057,12 @@
{:else if engineKind === "qwen"}
<div class="pcr-ip-hint">follows your instruction inside the mask only — unmasked pixels stay untouched; the region renders at full model resolution</div>
{/if}
{#if realismAvailable}
<label class="pcr-ip-hint" style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:6px;">
<input type="checkbox" bind:checked={realism} />
Realism — abliterated encoder + realism/bypass LoRAs (NSFW-capable)
</label>
{/if}
</div>
{/if}
{#if caps?.conditions && engineKind !== "qwen" && engineKind !== "flux1" && engineKind !== "krea2"}
@@ -70,6 +70,10 @@
: engineEntry.architecture === "krea2" ? "krea2"
: engineEntry.architecture === "flux" ? "flux1" : "sdxl")
: (engineSel === "source" ? "source" : "plain"));
// Realism mode: a krea2_turbo-only recipe (abliterated encoder + realism/bypass
// LoRAs). Tile upscale keeps qwen_image_vae (seam-safe) and USDU's sampler.
let realism = $state(false);
let realismAvailable = $derived(engineKind === "krea2" && /turbo/i.test(engineEntry?.filename || ""));
let sourceGraftable = $derived(engineKind === "source" && !!caps?.graftable);
// Tile engines share the USDU recipe surface (denoise/climb/tile knobs);
// qwen is the whole-frame re-render with its own reduced surface.
@@ -486,6 +490,7 @@
if (tileEngine && climbStage !== "ultrasharp") base.climbStage = climbStage;
if (engineKind !== "source" && climbModel) base.climbModel = climbModel;
if (engineKind === "source" && caps?.architecture === "flux2") base.preserveDefocus = preserveDefocus;
if (realismAvailable) base.realism = realism;
if (tileEngine || engineKind === "qwen") {
// Engine graphs have no source conditioning to fall back on — always
// send the current text (empty → the builder's per-arch constant).
@@ -831,6 +836,12 @@
{:else if engineKind === "plain"}
<span class="pcr-up-cond-hint">deterministic enlargement with the climb model below — add a Restore step for degraded sources (webcam, jpeg, blur)</span>
{/if}
{#if realismAvailable}
<label class="pcr-up-cond-hint" style="display:flex;align-items:center;gap:6px;cursor:pointer;margin-top:6px;">
<input type="checkbox" bind:checked={realism} />
Realism — abliterated encoder + realism/bypass LoRAs (NSFW-capable)
</label>
{/if}
</div>
</div>
{/if}
@@ -23,6 +23,7 @@
onSwitchVersion,
onOpenPicker,
onInjectNode,
onApplyStyleLora,
onApplyTemplate,
captureWorkflowGraph,
readWidgetValue,
@@ -423,6 +424,10 @@
onSave={handleSave}
{onReopen}
{onInjectNode}
{pcNode}
architecture={detectedArch}
family={currentFamily}
{onApplyStyleLora}
{onClose} />
</div>
@@ -3,6 +3,7 @@
// resolution presets, range mode, save/apply/add/edit footer.
import "./model-panel-shared.css";
import { api } from "/scripts/api.js";
import SettingsSlider from "./SettingsSlider.svelte";
import RangeSlider from "./RangeSlider.svelte";
import MultiSelectCombo from "./MultiSelectCombo.svelte";
@@ -20,6 +21,10 @@
onSave,
onReopen,
onInjectNode,
pcNode = null,
architecture = "",
family = "",
onApplyStyleLora = () => {},
onClose,
} = $props();
@@ -250,6 +255,105 @@
// Injection menu
let addMenuOpen = $state(false);
let addSubmenu = $state(null); // the group (e.g. ControlNet) drilled into
// ── Krea 2 style LoRA section (always present on Krea 2 Turbo) ──────
// A single swappable model-only LoRA + its trigger word, served from the
// scope-filtered lora catalog. Selecting downloads-if-missing then swaps the
// node + prompt trigger via onApplyStyleLora.
const showStyleSection = $derived(architecture === "krea2" && family === "krea2_turbo");
let styleCatalog = $state([]);
let styleInstalled = $state({}); // filename → exists
let styleSelected = $state(pcNode?.properties?.pcrStyleLora || "");
let styleStrength = $state(1.0);
let styleBusy = $state(false);
let styleStatus = $state("");
$effect(() => {
if (!showStyleSection) return;
let cancelled = false;
(async () => {
try {
const res = await fetch(`/promptchain/loras/catalog?architecture=${encodeURIComponent(architecture)}&family=${encodeURIComponent(family)}`);
const data = await res.json();
if (cancelled) return;
styleCatalog = data.loras || [];
const cur = styleCatalog.find(l => l.id === styleSelected);
if (cur) styleStrength = cur.strength_default ?? 1.0;
await refreshStyleInstalled();
} catch (e) { console.warn("[PromptChain] style catalog load failed:", e); }
})();
return () => { cancelled = true; };
});
async function refreshStyleInstalled() {
if (!styleCatalog.length) return;
try {
const res = await fetch("/promptchain/models/check-files", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ files: styleCatalog.map(l => ({ filename: l.filename, folder: l.folder || "loras", size_bytes: l.size_bytes || 0 })) }),
});
const data = await res.json();
const map = {};
for (const r of data.results || []) map[r.filename] = r.exists;
styleInstalled = map;
} catch (e) { console.warn("[PromptChain] style install check failed:", e); }
}
// Download a missing style via the shared civitai route; resolves on the WS
// "done" event for this file (event-driven, no polling).
function downloadStyle(lora) {
return new Promise((resolve) => {
const s = lora.source || {};
const url = s.provider === "huggingface"
? `https://huggingface.co/${s.repo}/resolve/main/${s.path}`
: (s.download_url || s.url || "");
if (!url) { resolve(false); return; }
const cleanup = () => {
api.removeEventListener("promptchain_download_progress", onProgress);
api.removeEventListener("promptchain_download_done", onDone);
};
function onProgress({ detail }) {
if (detail.filename !== lora.filename) return;
styleStatus = `Downloading ${lora.label}: ${detail.progress}%`;
}
function onDone({ detail }) {
if (detail.filename !== lora.filename) return;
cleanup();
resolve(detail.status === "completed");
}
api.addEventListener("promptchain_download_progress", onProgress);
api.addEventListener("promptchain_download_done", onDone);
fetch("/promptchain/civitai/download", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, filename: lora.filename, folder: lora.folder || "loras" }),
}).then(r => r.json()).then(d => {
if (d.error) { cleanup(); styleStatus = d.error; resolve(false); }
}).catch(err => { cleanup(); styleStatus = err.message; resolve(false); });
});
}
async function onStyleChange(id) {
styleSelected = id;
if (!id) { onApplyStyleLora(null, styleStrength); styleStatus = ""; return; }
const lora = styleCatalog.find(l => l.id === id);
if (!lora) return;
styleStrength = lora.strength_default ?? styleStrength ?? 1.0;
if (!styleInstalled[lora.filename]) {
styleBusy = true;
const ok = await downloadStyle(lora);
styleBusy = false;
if (!ok) { styleStatus = styleStatus || "Download failed"; styleSelected = pcNode?.properties?.pcrStyleLora || ""; return; }
styleInstalled = { ...styleInstalled, [lora.filename]: true };
styleStatus = "";
}
onApplyStyleLora(lora, styleStrength);
}
function onStyleStrength(v) {
styleStrength = v;
const lora = styleCatalog.find(l => l.id === styleSelected);
if (lora && styleInstalled[lora.filename]) onApplyStyleLora(lora, v);
}
let editMenuOpen = $state(false);
let injectable = $derived(injectableNodes());
@@ -517,6 +621,37 @@
{/each}
</div>
{/each}
{#if showStyleSection}
<div class="pcr-model-panel-section">
<div class="pcr-model-panel-section-title">Style</div>
<div class="pcr-model-panel-row">
<span class="pcr-model-panel-label">LoRA</span>
<div class="pcr-model-panel-combo-wrap">
<select class="pcr-model-panel-select"
disabled={styleBusy}
onchange={(e) => onStyleChange(e.target.value)}>
<option value="" selected={!styleSelected}>None</option>
{#each styleCatalog as l}
<option value={l.id} selected={l.id === styleSelected}>
{l.label}{styleInstalled[l.filename] ? "" : " ⬇"}
</option>
{/each}
</select>
</div>
</div>
{#if styleSelected}
<div class="pcr-model-panel-row">
<span class="pcr-model-panel-label">strength</span>
<SettingsSlider min={0} max={1.5} step={0.05} value={styleStrength}
onChange={(v) => onStyleStrength(v)} />
</div>
{/if}
{#if styleStatus}
<div class="pcr-model-panel-row"><span class="pcr-style-status">{styleStatus}</span></div>
{/if}
</div>
{/if}
</div>
{#if saveError}
@@ -640,6 +775,7 @@
letter-spacing: 0;
}
.pcr-section-expand-toggle:hover { color: #999; }
.pcr-style-status { font-size: 11px; color: #5dcaff; }
/* widget rows */
.pcr-model-panel-row {