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:
@@ -8,6 +8,7 @@ data/*
|
||||
!data/prompts/
|
||||
!data/models/
|
||||
!data/templates/
|
||||
!data/loras/
|
||||
!data/tag-builder/
|
||||
data/models/_hash_index.json
|
||||
cache/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)})
|
||||
@@ -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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
@@ -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}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+1888
-758
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user