Sync to registry release 0.1.1: pyproject + requirements (Registry manifest), .comfyignore, RIFE-only Frame-Interpolation, S1 (no runtime pip/git), comfy node validate fixes, insightface>=1.0 pin
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Files excluded from the published ComfyUI Registry tarball (gitignore syntax).
|
||||
# Dev-only / build artifacts — never needed by an installed pack.
|
||||
#
|
||||
# NOTE: bundled_packs/, data/ (tag DB + thumbs), and js/ assets stay IN — they're
|
||||
# the batteries-included feature set and are intentionally shipped.
|
||||
|
||||
# dev test harnesses
|
||||
tests/
|
||||
|
||||
# comfy-node-publish build artifact
|
||||
node.zip
|
||||
|
||||
# dev-only natlang probe scripts — never imported at runtime. The 6 runtime-imported
|
||||
# scripts (natlang_decompose_probe, natlang_compose_from_plan, natlang_scene_composer,
|
||||
# natlang_hybrid_v1, natlang_rails_v2, tag_rails_v1) are deliberately NOT listed.
|
||||
scripts/natlang_char_router_probe.py
|
||||
scripts/natlang_fragment_rewrite_harness.py
|
||||
scripts/natlang_harmonize_probe.py
|
||||
scripts/natlang_locate_infill_probe.py
|
||||
scripts/natlang_planner_probe.py
|
||||
scripts/natlang_resolve_probe.py
|
||||
scripts/natlang_scan_probe.py
|
||||
scripts/natlang_vibe_probe.py
|
||||
scripts/natlang_dispatch.py
|
||||
@@ -30,3 +30,5 @@ cache/
|
||||
# dev-only probes/harnesses/tests moved out of scripts/ (which keeps only the
|
||||
# runtime-imported modules) — kept local, never shipped
|
||||
/_harness/
|
||||
# comfy-node-publish build artifact (the uploaded tarball) — never commit it
|
||||
/node.zip
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ from .nodes.pose_studio import PoseStudioNode
|
||||
from .nodes.attention_couple import AttentionCoupleNode
|
||||
from .nodes.regional_detailer import RegionalDetailerNode, MaskedDetailNode
|
||||
from .nodes.regional_conditioning import RegionalConditioningNode
|
||||
from .nodes.region_box import RegionBoxNode
|
||||
from .nodes.defocus_mask import DefocusMaskNode
|
||||
from .nodes.ideogram_sampler import IdeogramSamplerNode
|
||||
from .nodes.ideogram_caption import IdeogramCaptionNode
|
||||
@@ -54,6 +55,7 @@ from .core import pose_api # noqa: F401
|
||||
from .core import inpaint_files # noqa: F401 (age-sweeps input/promptchain_inpaint at startup)
|
||||
from .core import upscale_api # noqa: F401
|
||||
from .core import edit_api # noqa: F401
|
||||
from .core import extend_api # noqa: F401
|
||||
from .core import modal_setup_api # noqa: F401
|
||||
from .core import subject_api # noqa: F401
|
||||
from .core import install_sections # noqa: F401
|
||||
@@ -222,7 +224,7 @@ _threading.Thread(target=_preload, daemon=True).start()
|
||||
class PromptChainExtension(ComfyExtension):
|
||||
@override
|
||||
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||
return [PromptChainNode, PoseStudioNode, AttentionCoupleNode, RegionalDetailerNode, MaskedDetailNode, RegionalConditioningNode, DefocusMaskNode, IdeogramSamplerNode, IdeogramCaptionNode]
|
||||
return [PromptChainNode, PoseStudioNode, AttentionCoupleNode, RegionalDetailerNode, MaskedDetailNode, RegionalConditioningNode, RegionBoxNode, DefocusMaskNode, IdeogramSamplerNode, IdeogramCaptionNode]
|
||||
|
||||
|
||||
async def comfy_entrypoint() -> PromptChainExtension:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Fannovel16
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from .other_nodes import Gradually_More_Denoise_KSampler
|
||||
|
||||
# Trimmed to RIFE only: PromptChain's Smooth/Extend use "RIFE VFI" exclusively.
|
||||
# The other interpolation models were never wired up and carried unclear/
|
||||
# non-commercial upstream licenses, so they can't ride along in a redistributed pack.
|
||||
from vfi_models.rife import RIFE_VFI
|
||||
from vfi_utils import MakeInterpolationStateList, FloatToInt
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"KSampler Gradually Adding More Denoise (efficient)": Gradually_More_Denoise_KSampler,
|
||||
"RIFE VFI": RIFE_VFI,
|
||||
"Make Interpolation State List": MakeInterpolationStateList,
|
||||
"VFI FloatToInt": FloatToInt
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RIFE VFI": "RIFE VFI (recommend rife4.25+)"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#Plz don't delete this file, just edit it when neccessary.
|
||||
ckpts_path: "./ckpts"
|
||||
ops_backend: "cupy" #Either "taichi" or "cupy"
|
||||
@@ -0,0 +1,88 @@
|
||||
import latent_preview
|
||||
import comfy
|
||||
import einops
|
||||
import torch
|
||||
|
||||
def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
|
||||
device = comfy.model_management.get_torch_device()
|
||||
latent_image = latent["samples"]
|
||||
|
||||
if disable_noise:
|
||||
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
|
||||
else:
|
||||
batch_inds = latent["batch_index"] if "batch_index" in latent else None
|
||||
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
|
||||
|
||||
noise_mask = None
|
||||
if "noise_mask" in latent:
|
||||
noise_mask = latent["noise_mask"]
|
||||
|
||||
preview_format = "JPEG"
|
||||
if preview_format not in ["JPEG", "PNG"]:
|
||||
preview_format = "JPEG"
|
||||
|
||||
previewer = latent_preview.get_previewer(device, model.model.latent_format)
|
||||
|
||||
pbar = comfy.utils.ProgressBar(steps)
|
||||
def callback(step, x0, x, total_steps):
|
||||
preview_bytes = None
|
||||
if previewer:
|
||||
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
|
||||
pbar.update_absolute(step + 1, total_steps, preview_bytes)
|
||||
|
||||
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
||||
denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
|
||||
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, seed=seed)
|
||||
out = latent.copy()
|
||||
out["samples"] = samples
|
||||
return (out, )
|
||||
|
||||
class Gradually_More_Denoise_KSampler:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required":
|
||||
{"model": ("MODEL",),
|
||||
"positive": ("CONDITIONING", ),
|
||||
"negative": ("CONDITIONING", ),
|
||||
"latent_image": ("LATENT", ),
|
||||
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
||||
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
|
||||
|
||||
"start_denoise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"denoise_increment": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.1}),
|
||||
"denoise_increment_steps": ("INT", {"default": 20, "min": 1, "max": 10000})
|
||||
},
|
||||
"optional": { "optional_vae": ("VAE",) }
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", )
|
||||
RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", )
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "sample"
|
||||
CATEGORY = "ComfyUI-Frame-Interpolation/others"
|
||||
|
||||
def sample(self, model, positive, negative, latent_image, optional_vae,
|
||||
seed, steps, cfg, sampler_name, scheduler,start_denoise, denoise_increment, denoise_increment_steps):
|
||||
if start_denoise + denoise_increment * denoise_increment_steps > 1.0:
|
||||
raise Exception(f"Max denoise strength can't over 1.0 (start_denoise={start_denoise}, denoise_increment={denoise_increment}, denoise_increment_steps={denoise_increment_steps}")
|
||||
|
||||
copied_latent = latent_image.copy()
|
||||
out_samples = []
|
||||
|
||||
for latent_sample in copied_latent["samples"]:
|
||||
latent = {"samples": einops.rearrange(latent_sample, "c h w -> 1 c h w")}
|
||||
#Latent's shape is NCHW
|
||||
gradually_denoising_samples = [
|
||||
common_ksampler(
|
||||
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=start_denoise + denoise_increment * i
|
||||
)[0]["samples"]
|
||||
for i in range(denoise_increment_steps)
|
||||
]
|
||||
out_samples.extend(gradually_denoising_samples)
|
||||
|
||||
copied_latent["samples"] = torch.cat(out_samples, dim=0)
|
||||
return (model, positive, negative, copied_latent, optional_vae)
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "comfyui-frame-interpolation"
|
||||
description = "A custom node suite for Video Frame Interpolation in ComfyUI"
|
||||
version = "1.0.11"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/Fannovel16/ComfyUI-Frame-Interpolation"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "fannovel16"
|
||||
DisplayName = "ComfyUI-Frame-Interpolation"
|
||||
Icon = ""
|
||||
@@ -0,0 +1 @@
|
||||
einops
|
||||
@@ -0,0 +1,239 @@
|
||||
import gc
|
||||
import torch
|
||||
import pathlib
|
||||
from vfi_utils import load_file_from_github_release, preprocess_frames, postprocess_frames, InterpolationStateList
|
||||
import typing
|
||||
from comfy.model_management import get_torch_device, soft_empty_cache
|
||||
from packaging import version
|
||||
|
||||
MODEL_TYPE = pathlib.Path(__file__).parent.name
|
||||
CKPT_NAME_VER_DICT = {
|
||||
"rife47.pth": "4.7",
|
||||
"rife49.pth": "4.7",
|
||||
"rife417.pth": "4.17",
|
||||
"rife426.pth": "4.26",
|
||||
"sudo_rife4_269.662_testV1_scale1.pth": "4.0",
|
||||
# Arch 4.10 doesn't work due to state dict mismatch
|
||||
# "rife410.pth": "4.10",
|
||||
# "rife411.pth": "4.10",
|
||||
# "rife412.pth": "4.10"
|
||||
}
|
||||
|
||||
DTYPE_OPTIONS = ["float32", "float16", "bfloat16"]
|
||||
DTYPE_MAP = {
|
||||
"float32": torch.float32,
|
||||
"float16": torch.float16,
|
||||
"bfloat16": torch.bfloat16,
|
||||
}
|
||||
|
||||
# Module-level model cache: avoids reloading weights on every node execution.
|
||||
# Key: (ckpt_name, dtype, torch_compile) — invalidated if any of these change.
|
||||
_model_cache: typing.Dict[typing.Tuple, torch.nn.Module] = {}
|
||||
|
||||
|
||||
class RIFE_VFI:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"ckpt_name": (
|
||||
sorted(list(CKPT_NAME_VER_DICT.keys()), key=lambda ckpt_name: version.parse(CKPT_NAME_VER_DICT[ckpt_name])),
|
||||
{"default": "rife49.pth"}
|
||||
),
|
||||
"frames": ("IMAGE", ),
|
||||
"clear_cache_after_n_frames": ("INT", {"default": 10, "min": 1, "max": 1000}),
|
||||
"multiplier": ("INT", {"default": 2, "min": 1}),
|
||||
"fast_mode": ("BOOLEAN", {"default": True}),
|
||||
"ensemble": ("BOOLEAN", {"default": True}),
|
||||
"scale_factor": ([0.25, 0.5, 1.0, 2.0, 4.0], {"default": 1.0}),
|
||||
# Opt 1: Precision — float32 is safe, float16 is ~2x faster/half VRAM,
|
||||
# bfloat16 is safer than float16 on Ampere+ GPUs (RTX 30xx+)
|
||||
"dtype": (DTYPE_OPTIONS, {"default": "float32"}),
|
||||
# Opt 2: torch.compile() — JIT-compiles the model graph for 10-30% speedup.
|
||||
# First inference will be slow (compilation), subsequent ones faster.
|
||||
# Requires PyTorch 2.0+. Disable if you hit errors on older builds.
|
||||
"torch_compile": ("BOOLEAN", {"default": False,
|
||||
"tooltip": "Compile the model with torch.compile() for 10-30% faster inference "
|
||||
"after the first (warm-up) run. Requires PyTorch 2.0+."}),
|
||||
# Opt 7: Batch tasks — process multiple (pair, timestep) tasks per GPU call.
|
||||
# Each task is one interpolated frame. Batching amortises kernel launch
|
||||
# overhead; higher values improve throughput but use more VRAM.
|
||||
# 1 = one interpolated frame per call (safest).
|
||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64,
|
||||
"tooltip": "Number of interpolation tasks per GPU call. "
|
||||
"Higher values improve throughput but use more VRAM. "
|
||||
"Set to 1 for the most conservative behaviour."}),
|
||||
},
|
||||
"optional": {
|
||||
"optional_interpolation_states": ("INTERPOLATION_STATES", )
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", )
|
||||
FUNCTION = "vfi"
|
||||
CATEGORY = "ComfyUI-Frame-Interpolation/VFI"
|
||||
|
||||
def vfi(
|
||||
self,
|
||||
ckpt_name: typing.AnyStr,
|
||||
frames: torch.Tensor,
|
||||
clear_cache_after_n_frames: int = 10,
|
||||
multiplier: typing.SupportsInt = 2,
|
||||
fast_mode: bool = False,
|
||||
ensemble: bool = False,
|
||||
scale_factor: float = 1.0,
|
||||
dtype: str = "float32",
|
||||
torch_compile: bool = False,
|
||||
batch_size: int = 1,
|
||||
optional_interpolation_states: InterpolationStateList = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Perform video frame interpolation using a given checkpoint model.
|
||||
|
||||
Args:
|
||||
ckpt_name (str): The name of the checkpoint model to use.
|
||||
frames (torch.Tensor): A tensor containing input video frames.
|
||||
clear_cache_after_n_frames (int, optional): The number of frames to process before clearing CUDA cache
|
||||
to prevent memory overflow. Defaults to 10. Lower numbers are safer but mean more processing time.
|
||||
How high you should set it depends on how many input frames there are, input resolution (after upscaling),
|
||||
how many times you want to multiply them, and how long you're willing to wait for the process to complete.
|
||||
multiplier (int, optional): The multiplier for each input frame. 60 input frames * 2 = 120 output frames. Defaults to 2.
|
||||
dtype (str, optional): Floating-point precision for inference.
|
||||
"float32" — full precision, safest (default).
|
||||
"float16" — half precision, ~2x faster and ~50% less VRAM, may have minor artifacts.
|
||||
"bfloat16" — brain float16, better numeric range than float16, requires Ampere+ GPU (RTX 30xx+).
|
||||
torch_compile (bool, optional): Compile the model with torch.compile() for 10-30% speedup.
|
||||
The first inference call will be slow (compilation warm-up); subsequent calls are faster.
|
||||
Requires PyTorch 2.0+. Defaults to False.
|
||||
batch_size (int, optional): Number of interpolation tasks per GPU call. Each task produces one
|
||||
intermediate frame. Higher values improve throughput but use more VRAM. Defaults to 1.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the output interpolated frames.
|
||||
"""
|
||||
from .rife_arch import IFNet
|
||||
|
||||
model_path = load_file_from_github_release(MODEL_TYPE, ckpt_name)
|
||||
arch_ver = CKPT_NAME_VER_DICT[ckpt_name]
|
||||
torch_dtype = DTYPE_MAP[dtype]
|
||||
device = get_torch_device()
|
||||
|
||||
# 4.26 disable ensemble
|
||||
if arch_ver == "4.26":
|
||||
ensemble = False # 4.26 版本不支持 ensemble
|
||||
|
||||
# Cache the model by (ckpt_name, dtype, torch_compile) so repeated node
|
||||
# executions skip the load_state_dict + device transfer entirely.
|
||||
cache_key = (ckpt_name, dtype, torch_compile)
|
||||
if cache_key not in _model_cache:
|
||||
interpolation_model = IFNet(arch_ver=arch_ver)
|
||||
interpolation_model.load_state_dict(torch.load(model_path, weights_only=False))
|
||||
if torch_dtype != torch.float32:
|
||||
interpolation_model = interpolation_model.to(torch_dtype)
|
||||
interpolation_model.eval().to(device)
|
||||
# Opt 2: torch.compile() — JIT-compiles the model for 10-30% faster inference.
|
||||
# The first call triggers compilation (slow); all subsequent calls use the cached graph.
|
||||
if torch_compile:
|
||||
interpolation_model = torch.compile(interpolation_model)
|
||||
_model_cache[cache_key] = interpolation_model
|
||||
print(f"Comfy-VFI: Loaded and cached model {ckpt_name} ({dtype}{'+ torch.compile' if torch_compile else ''})")
|
||||
else:
|
||||
interpolation_model = _model_cache[cache_key]
|
||||
print(f"Comfy-VFI: Using cached model {ckpt_name} ({dtype}{'+ torch.compile' if torch_compile else ''})")
|
||||
|
||||
frames = preprocess_frames(frames)
|
||||
|
||||
# Normalise multiplier to a per-pair list
|
||||
n_pairs = len(frames) - 1
|
||||
if isinstance(multiplier, int):
|
||||
multipliers = [int(multiplier)] * n_pairs
|
||||
else:
|
||||
multipliers = list(map(int, multiplier))
|
||||
multipliers += [2] * (n_pairs - len(multipliers))
|
||||
|
||||
# 4.26 scale_list
|
||||
if arch_ver == "4.26":
|
||||
scale_list = [16 / scale_factor, 8 / scale_factor, 4 / scale_factor, 2 / scale_factor, 1 / scale_factor]
|
||||
else:
|
||||
scale_list = [8 / scale_factor, 4 / scale_factor, 2 / scale_factor, 1 / scale_factor]
|
||||
|
||||
# Build a flat list of all (pair_idx, timestep) tasks, skipping excluded pairs.
|
||||
# Each task produces exactly one intermediate frame.
|
||||
tasks: typing.List[typing.Tuple[int, float]] = []
|
||||
tasks_remaining_per_pair: typing.Dict[int, int] = {}
|
||||
for pair_idx in range(n_pairs):
|
||||
if optional_interpolation_states is not None and optional_interpolation_states.is_frame_skipped(pair_idx):
|
||||
tasks_remaining_per_pair[pair_idx] = 0
|
||||
continue
|
||||
m = multipliers[pair_idx]
|
||||
n_steps = max(m - 1, 0)
|
||||
tasks_remaining_per_pair[pair_idx] = n_steps
|
||||
for step in range(1, m):
|
||||
tasks.append((pair_idx, step / m))
|
||||
|
||||
# Storage for intermediate frames, keyed by pair index
|
||||
results: typing.Dict[int, typing.List[torch.Tensor]] = {i: [] for i in range(n_pairs)}
|
||||
|
||||
frames_processed_since_cache_clear = 0
|
||||
pos = 0
|
||||
|
||||
# Opt 5: inference_mode disables autograd tracking entirely — lower overhead than no_grad().
|
||||
# Opt 7: process batch_size tasks per GPU call. IFNet supports batched tensor timesteps,
|
||||
# so multiple (pair, timestep) combinations can be inferred in a single forward pass.
|
||||
with torch.inference_mode():
|
||||
while pos < len(tasks):
|
||||
batch_tasks = tasks[pos : pos + batch_size]
|
||||
|
||||
frame0_list, frame1_list, timestep_list = [], [], []
|
||||
for pair_idx, dt in batch_tasks:
|
||||
frame0_list.append(frames[pair_idx : pair_idx + 1])
|
||||
frame1_list.append(frames[pair_idx + 1 : pair_idx + 2])
|
||||
timestep_list.append(dt)
|
||||
|
||||
frame0_batch = torch.cat(frame0_list, dim=0).to(device, dtype=torch_dtype)
|
||||
frame1_batch = torch.cat(frame1_list, dim=0).to(device, dtype=torch_dtype)
|
||||
# Batched timestep tensor: shape [B, 1, 1, 1] — IFNet expands it internally
|
||||
timestep_tensor = torch.tensor(timestep_list, dtype=torch_dtype, device=device).view(-1, 1, 1, 1)
|
||||
|
||||
middle_frames = interpolation_model(
|
||||
frame0_batch,
|
||||
frame1_batch,
|
||||
timestep_tensor,
|
||||
scale_list,
|
||||
fast_mode,
|
||||
ensemble,
|
||||
).clamp(0, 1).detach().cpu()
|
||||
|
||||
for idx, (pair_idx, _) in enumerate(batch_tasks):
|
||||
results[pair_idx].append(middle_frames[idx : idx + 1].to(dtype=torch_dtype))
|
||||
tasks_remaining_per_pair[pair_idx] -= 1
|
||||
# Clear cache after finishing each pair's worth of tasks
|
||||
if tasks_remaining_per_pair[pair_idx] == 0:
|
||||
frames_processed_since_cache_clear += 1
|
||||
if frames_processed_since_cache_clear >= clear_cache_after_n_frames:
|
||||
print("Comfy-VFI: Clearing cache...", end=' ')
|
||||
soft_empty_cache()
|
||||
gc.collect()
|
||||
frames_processed_since_cache_clear = 0
|
||||
print("Done cache clearing")
|
||||
|
||||
pos += len(batch_tasks)
|
||||
|
||||
# Assemble output: each original frame followed by its interpolated frames
|
||||
output_frames: typing.List[torch.Tensor] = []
|
||||
for pair_idx in range(n_pairs):
|
||||
output_frames.append(frames[pair_idx : pair_idx + 1].to(dtype=torch_dtype))
|
||||
for mid in results[pair_idx]:
|
||||
output_frames.append(mid)
|
||||
output_frames.append(frames[-1:].to(dtype=torch_dtype))
|
||||
|
||||
print("Comfy-VFI: Final clearing cache...", end=' ')
|
||||
soft_empty_cache()
|
||||
print("Done cache clearing")
|
||||
print(f"Comfy-VFI done! {sum(len(v) for v in results.values()) + len(frames)} frames generated")
|
||||
|
||||
# Always return float32 — numpy and all downstream ComfyUI nodes require it
|
||||
out_tensor = torch.cat(output_frames, dim=0).to(torch.float32)
|
||||
return (postprocess_frames(out_tensor),)
|
||||
@@ -0,0 +1,732 @@
|
||||
"""
|
||||
26-Dez-21
|
||||
https://github.com/hzwer/Practical-RIFE
|
||||
https://github.com/hzwer/Practical-RIFE/blob/main/model/warplayer.py
|
||||
https://github.com/HolyWu/vs-rife/blob/master/vsrife/__init__.py
|
||||
"""
|
||||
from torch.nn.parallel import DistributedDataParallel as DDP
|
||||
from torch.optim import AdamW
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import warnings
|
||||
from comfy.model_management import get_torch_device
|
||||
|
||||
device = get_torch_device()
|
||||
backwarp_tenGrid = {}
|
||||
|
||||
|
||||
class ResConv(nn.Module):
|
||||
def __init__(self, c, dilation=1):
|
||||
super(ResConv, self).__init__()
|
||||
self.conv = nn.Conv2d(c, c, 3, 1, dilation, dilation=dilation, groups=1)
|
||||
self.beta = nn.Parameter(torch.ones((1, c, 1, 1)), requires_grad=True)
|
||||
self.relu = nn.LeakyReLU(0.2, True)
|
||||
|
||||
def forward(self, x):
|
||||
return self.relu(self.conv(x) * self.beta + x)
|
||||
|
||||
|
||||
def warp(tenInput, tenFlow):
|
||||
k = (str(tenFlow.device), str(tenFlow.size()))
|
||||
if k not in backwarp_tenGrid:
|
||||
tenHorizontal = (
|
||||
torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=device)
|
||||
.view(1, 1, 1, tenFlow.shape[3])
|
||||
.expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
|
||||
)
|
||||
tenVertical = (
|
||||
torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=device)
|
||||
.view(1, 1, tenFlow.shape[2], 1)
|
||||
.expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
|
||||
)
|
||||
backwarp_tenGrid[k] = torch.cat([tenHorizontal, tenVertical], 1).to(device)
|
||||
|
||||
tenFlow = torch.cat(
|
||||
[
|
||||
tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
|
||||
tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0),
|
||||
],
|
||||
1,
|
||||
)
|
||||
|
||||
g = (backwarp_tenGrid[k] + tenFlow).permute(0, 2, 3, 1)
|
||||
|
||||
if g.dtype != tenInput.dtype:
|
||||
g = g.to(tenInput.dtype)
|
||||
|
||||
padding_mode = "border"
|
||||
if device.type == "mps":
|
||||
# https://github.com/pytorch/pytorch/issues/125098
|
||||
padding_mode = "zeros"
|
||||
g = g.clamp(-1, 1)
|
||||
return torch.nn.functional.grid_sample(
|
||||
input=tenInput,
|
||||
grid=g,
|
||||
mode="bilinear",
|
||||
padding_mode=padding_mode,
|
||||
align_corners=True,
|
||||
)
|
||||
|
||||
|
||||
def conv(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
padding=1,
|
||||
dilation=1,
|
||||
arch_ver="4.0",
|
||||
):
|
||||
if arch_ver == "4.0":
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=True,
|
||||
),
|
||||
nn.PReLU(out_planes),
|
||||
)
|
||||
if arch_ver in ["4.2", "4.3", "4.5", "4.6", "4.7", "4.10", "4.17", "4.26"]:
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=True,
|
||||
),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
)
|
||||
|
||||
|
||||
def conv_woact(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def conv_woact(in_planes, out_planes, kernel_size=3, stride=1, padding=1, dilation=1):
|
||||
return nn.Sequential(
|
||||
nn.Conv2d(
|
||||
in_planes,
|
||||
out_planes,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
padding=padding,
|
||||
dilation=dilation,
|
||||
bias=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def deconv(in_planes, out_planes, kernel_size=4, stride=2, padding=1, arch_ver="4.0"):
|
||||
if arch_ver == "4.0":
|
||||
return nn.Sequential(
|
||||
torch.nn.ConvTranspose2d(
|
||||
in_channels=in_planes,
|
||||
out_channels=out_planes,
|
||||
kernel_size=4,
|
||||
stride=2,
|
||||
padding=1,
|
||||
bias=True,
|
||||
),
|
||||
nn.PReLU(out_planes),
|
||||
)
|
||||
if arch_ver in ["4.2", "4.3", "4.5", "4.6", "4.7", "4.10", "4.17", "4.26"]:
|
||||
return nn.Sequential(
|
||||
torch.nn.ConvTranspose2d(
|
||||
in_channels=in_planes,
|
||||
out_channels=out_planes,
|
||||
kernel_size=4,
|
||||
stride=2,
|
||||
padding=1,
|
||||
bias=True,
|
||||
),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
)
|
||||
|
||||
|
||||
class Conv2(nn.Module):
|
||||
def __init__(self, in_planes, out_planes, stride=2, arch_ver="4.0"):
|
||||
super(Conv2, self).__init__()
|
||||
self.conv1 = conv(in_planes, out_planes, 3, stride, 1, arch_ver=arch_ver)
|
||||
self.conv2 = conv(out_planes, out_planes, 3, 1, 1, arch_ver=arch_ver)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.conv1(x)
|
||||
x = self.conv2(x)
|
||||
return x
|
||||
|
||||
|
||||
class IFBlock(nn.Module):
|
||||
def __init__(self, in_planes, c=64, arch_ver="4.0"):
|
||||
super(IFBlock, self).__init__()
|
||||
self.arch_ver = arch_ver
|
||||
self.conv0 = nn.Sequential(
|
||||
conv(in_planes, c // 2, 3, 2, 1, arch_ver=arch_ver),
|
||||
conv(c // 2, c, 3, 2, 1, arch_ver=arch_ver),
|
||||
)
|
||||
self.arch_ver = arch_ver
|
||||
|
||||
if arch_ver in ["4.0", "4.2", "4.3"]:
|
||||
self.convblock = nn.Sequential(
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
conv(c, c, arch_ver=arch_ver),
|
||||
)
|
||||
self.lastconv = nn.ConvTranspose2d(c, 5, 4, 2, 1)
|
||||
|
||||
if arch_ver in ["4.5", "4.6", "4.7", "4.10", "4.17"]:
|
||||
self.convblock = nn.Sequential(
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
)
|
||||
if arch_ver == "4.5":
|
||||
self.lastconv = nn.Sequential(
|
||||
nn.ConvTranspose2d(c, 4 * 5, 4, 2, 1), nn.PixelShuffle(2)
|
||||
)
|
||||
if arch_ver in ["4.6", "4.7", "4.10", "4.17"]:
|
||||
self.lastconv = nn.Sequential(
|
||||
nn.ConvTranspose2d(c, 4 * 6, 4, 2, 1), nn.PixelShuffle(2)
|
||||
)
|
||||
|
||||
# add 4.26 support
|
||||
if arch_ver in ["4.26"]:
|
||||
self.convblock = nn.Sequential(
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
ResConv(c),
|
||||
)
|
||||
self.lastconv = nn.Sequential(
|
||||
nn.ConvTranspose2d(c, 4*13, 4, 2, 1),
|
||||
nn.PixelShuffle(2)
|
||||
)
|
||||
|
||||
def forward(self, x, flow=None, scale=1):
|
||||
x = F.interpolate(
|
||||
x, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
if flow is not None:
|
||||
flow = (
|
||||
F.interpolate(
|
||||
flow, scale_factor=1.0 / scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
* 1.0
|
||||
/ scale
|
||||
)
|
||||
x = torch.cat((x, flow), 1)
|
||||
feat = self.conv0(x)
|
||||
if self.arch_ver == "4.0":
|
||||
feat = self.convblock(feat) + feat
|
||||
if self.arch_ver in ["4.2", "4.3", "4.5", "4.6", "4.7", "4.10", "4.17", "4.26"]:
|
||||
feat = self.convblock(feat)
|
||||
|
||||
tmp = self.lastconv(feat)
|
||||
if self.arch_ver in ["4.0", "4.2", "4.3"]:
|
||||
tmp = F.interpolate(
|
||||
tmp, scale_factor=scale * 2, mode="bilinear", align_corners=False
|
||||
)
|
||||
flow = tmp[:, :4] * scale * 2
|
||||
if self.arch_ver in ["4.5", "4.6", "4.7", "4.10", "4.17"]:
|
||||
tmp = F.interpolate(
|
||||
tmp, scale_factor=scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
flow = tmp[:, :4] * scale
|
||||
if self.arch_ver in ["4.26"]:
|
||||
tmp = F.interpolate(
|
||||
tmp, scale_factor=scale, mode="bilinear", align_corners=False
|
||||
)
|
||||
flow = tmp[:, :4] * scale
|
||||
mask = tmp[:, 4:5]
|
||||
feat_out = tmp[:, 5:]
|
||||
return flow, mask, feat_out
|
||||
mask = tmp[:, 4:5]
|
||||
return flow, mask
|
||||
|
||||
|
||||
class Contextnet(nn.Module):
|
||||
def __init__(self, arch_ver="4.0"):
|
||||
super(Contextnet, self).__init__()
|
||||
c = 16
|
||||
self.conv1 = Conv2(3, c, arch_ver=arch_ver)
|
||||
self.conv2 = Conv2(c, 2 * c, arch_ver=arch_ver)
|
||||
self.conv3 = Conv2(2 * c, 4 * c, arch_ver=arch_ver)
|
||||
self.conv4 = Conv2(4 * c, 8 * c, arch_ver=arch_ver)
|
||||
|
||||
def forward(self, x, flow):
|
||||
x = self.conv1(x)
|
||||
flow = (
|
||||
F.interpolate(flow, scale_factor=0.5, mode="bilinear", align_corners=False)
|
||||
* 0.5
|
||||
)
|
||||
f1 = warp(x, flow)
|
||||
x = self.conv2(x)
|
||||
flow = (
|
||||
F.interpolate(flow, scale_factor=0.5, mode="bilinear", align_corners=False)
|
||||
* 0.5
|
||||
)
|
||||
f2 = warp(x, flow)
|
||||
x = self.conv3(x)
|
||||
flow = (
|
||||
F.interpolate(flow, scale_factor=0.5, mode="bilinear", align_corners=False)
|
||||
* 0.5
|
||||
)
|
||||
f3 = warp(x, flow)
|
||||
x = self.conv4(x)
|
||||
flow = (
|
||||
F.interpolate(flow, scale_factor=0.5, mode="bilinear", align_corners=False)
|
||||
* 0.5
|
||||
)
|
||||
f4 = warp(x, flow)
|
||||
return [f1, f2, f3, f4]
|
||||
|
||||
|
||||
class Unet(nn.Module):
|
||||
def __init__(self, arch_ver="4.0"):
|
||||
super(Unet, self).__init__()
|
||||
c = 16
|
||||
self.down0 = Conv2(17, 2 * c, arch_ver=arch_ver)
|
||||
self.down1 = Conv2(4 * c, 4 * c, arch_ver=arch_ver)
|
||||
self.down2 = Conv2(8 * c, 8 * c, arch_ver=arch_ver)
|
||||
self.down3 = Conv2(16 * c, 16 * c, arch_ver=arch_ver)
|
||||
self.up0 = deconv(32 * c, 8 * c, arch_ver=arch_ver)
|
||||
self.up1 = deconv(16 * c, 4 * c, arch_ver=arch_ver)
|
||||
self.up2 = deconv(8 * c, 2 * c, arch_ver=arch_ver)
|
||||
self.up3 = deconv(4 * c, c, arch_ver=arch_ver)
|
||||
self.conv = nn.Conv2d(c, 3, 3, 1, 1)
|
||||
|
||||
def forward(self, img0, img1, warped_img0, warped_img1, mask, flow, c0, c1):
|
||||
s0 = self.down0(
|
||||
torch.cat((img0, img1, warped_img0, warped_img1, mask, flow), 1)
|
||||
)
|
||||
s1 = self.down1(torch.cat((s0, c0[0], c1[0]), 1))
|
||||
s2 = self.down2(torch.cat((s1, c0[1], c1[1]), 1))
|
||||
s3 = self.down3(torch.cat((s2, c0[2], c1[2]), 1))
|
||||
x = self.up0(torch.cat((s3, c0[3], c1[3]), 1))
|
||||
x = self.up1(torch.cat((x, s2), 1))
|
||||
x = self.up2(torch.cat((x, s1), 1))
|
||||
x = self.up3(torch.cat((x, s0), 1))
|
||||
x = self.conv(x)
|
||||
return torch.sigmoid(x)
|
||||
|
||||
|
||||
"""
|
||||
currently supports 4.0-4.12
|
||||
|
||||
4.0: 4.0, 4.1
|
||||
4.2: 4.2
|
||||
4.3: 4.3, 4.4
|
||||
4.5: 4.5
|
||||
4.6: 4.6
|
||||
4.7: 4.7, 4.8, 4.9
|
||||
4.10: 4.10 4.11 4.12
|
||||
"""
|
||||
class Head_417(nn.Module):
|
||||
def __init__(self):
|
||||
super(Head_417, self).__init__()
|
||||
self.cnn0 = nn.Conv2d(3, 32, 3, 2, 1)
|
||||
self.cnn1 = nn.Conv2d(32, 32, 3, 1, 1)
|
||||
self.cnn2 = nn.Conv2d(32, 32, 3, 1, 1)
|
||||
self.cnn3 = nn.ConvTranspose2d(32, 8, 4, 2, 1)
|
||||
self.relu = nn.LeakyReLU(0.2, True)
|
||||
|
||||
def forward(self, x, feat=False):
|
||||
x0 = self.cnn0(x)
|
||||
x = self.relu(x0)
|
||||
x1 = self.cnn1(x)
|
||||
x = self.relu(x1)
|
||||
x2 = self.cnn2(x)
|
||||
x = self.relu(x2)
|
||||
x3 = self.cnn3(x)
|
||||
if feat:
|
||||
return [x0, x1, x2, x3]
|
||||
return x3
|
||||
|
||||
|
||||
class Head(nn.Module):
|
||||
def __init__(self):
|
||||
super(Head, self).__init__()
|
||||
self.cnn0 = nn.Conv2d(3, 16, 3, 2, 1)
|
||||
self.cnn1 = nn.Conv2d(16, 16, 3, 1, 1)
|
||||
self.cnn2 = nn.Conv2d(16, 16, 3, 1, 1)
|
||||
self.cnn3 = nn.ConvTranspose2d(16, 4, 4, 2, 1)
|
||||
self.relu = nn.LeakyReLU(0.2, True)
|
||||
|
||||
def forward(self, x, feat=False):
|
||||
x0 = self.cnn0(x)
|
||||
x = self.relu(x0)
|
||||
x1 = self.cnn1(x)
|
||||
x = self.relu(x1)
|
||||
x2 = self.cnn2(x)
|
||||
x = self.relu(x2)
|
||||
x3 = self.cnn3(x)
|
||||
if feat:
|
||||
return [x0, x1, x2, x3]
|
||||
return x3
|
||||
|
||||
|
||||
class IFNet(nn.Module):
|
||||
def __init__(self, arch_ver="4.0"):
|
||||
super(IFNet, self).__init__()
|
||||
self.arch_ver = arch_ver
|
||||
if arch_ver in ["4.0", "4.2", "4.3", "4.5", "4.6"]:
|
||||
self.block0 = IFBlock(7, c=192, arch_ver=arch_ver)
|
||||
self.block1 = IFBlock(8 + 4, c=128, arch_ver=arch_ver)
|
||||
self.block2 = IFBlock(8 + 4, c=96, arch_ver=arch_ver)
|
||||
self.block3 = IFBlock(8 + 4, c=64, arch_ver=arch_ver)
|
||||
if arch_ver in ["4.7"]:
|
||||
self.block0 = IFBlock(7 + 8, c=192, arch_ver=arch_ver)
|
||||
self.block1 = IFBlock(8 + 4 + 8, c=128, arch_ver=arch_ver)
|
||||
self.block2 = IFBlock(8 + 4 + 8, c=96, arch_ver=arch_ver)
|
||||
self.block3 = IFBlock(8 + 4 + 8, c=64, arch_ver=arch_ver)
|
||||
self.encode = nn.Sequential(
|
||||
nn.Conv2d(3, 16, 3, 2, 1), nn.ConvTranspose2d(16, 4, 4, 2, 1)
|
||||
)
|
||||
if arch_ver in ["4.10", "4.17"]:
|
||||
self.block0 = IFBlock(7 + 16, c=192, arch_ver=arch_ver)
|
||||
self.block1 = IFBlock(8 + 4 + 16, c=128, arch_ver=arch_ver)
|
||||
self.block2 = IFBlock(8 + 4 + 16, c=96, arch_ver=arch_ver)
|
||||
self.block3 = IFBlock(8 + 4 + 16, c=64, arch_ver=arch_ver)
|
||||
if arch_ver == "4.10":
|
||||
self.encode = nn.Sequential(
|
||||
nn.Conv2d(3, 32, 3, 2, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.Conv2d(32, 32, 3, 1, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.Conv2d(32, 32, 3, 1, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.ConvTranspose2d(32, 8, 4, 2, 1),
|
||||
)
|
||||
else:
|
||||
self.encode = Head_417()
|
||||
|
||||
if arch_ver in ["old_4.10_delete_me"]:
|
||||
self.block0 = IFBlock(7 + 16, c=192)
|
||||
self.block1 = IFBlock(8 + 4 + 16, c=128)
|
||||
self.block2 = IFBlock(8 + 4 + 16, c=96)
|
||||
self.block3 = IFBlock(8 + 4 + 16, c=64)
|
||||
self.encode = nn.Sequential(
|
||||
nn.Conv2d(3, 32, 3, 2, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.Conv2d(32, 32, 3, 1, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.Conv2d(32, 32, 3, 1, 1),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
nn.ConvTranspose2d(32, 8, 4, 2, 1),
|
||||
)
|
||||
|
||||
# add 4.26 support
|
||||
if arch_ver in ["4.26"]:
|
||||
self.block0 = IFBlock(7+8, c=192, arch_ver=arch_ver)
|
||||
self.block1 = IFBlock(8+4+8+8, c=128, arch_ver=arch_ver)
|
||||
self.block2 = IFBlock(8+4+8+8, c=96, arch_ver=arch_ver)
|
||||
self.block3 = IFBlock(8+4+8+8, c=64, arch_ver=arch_ver)
|
||||
self.block4 = IFBlock(8+4+8+8, c=32, arch_ver=arch_ver)
|
||||
self.encode = Head()
|
||||
|
||||
|
||||
if arch_ver in ["4.0", "4.2", "4.3"]:
|
||||
self.contextnet = Contextnet(arch_ver=arch_ver)
|
||||
self.unet = Unet(arch_ver=arch_ver)
|
||||
self.arch_ver = arch_ver
|
||||
|
||||
def forward(
|
||||
self,
|
||||
img0,
|
||||
img1,
|
||||
timestep=0.5,
|
||||
scale_list=[16, 8, 4, 2, 1],
|
||||
training=True,
|
||||
fastmode=True,
|
||||
ensemble=False,
|
||||
return_flow=False,
|
||||
):
|
||||
img0 = torch.clamp(img0, 0, 1)
|
||||
img1 = torch.clamp(img1, 0, 1)
|
||||
|
||||
n, c, h, w = img0.shape
|
||||
ph = ((h - 1) // 64 + 1) * 64
|
||||
pw = ((w - 1) // 64 + 1) * 64
|
||||
padding = (0, pw - w, 0, ph - h)
|
||||
img0 = F.pad(img0, padding)
|
||||
img1 = F.pad(img1, padding)
|
||||
x = torch.cat((img0, img1), 1)
|
||||
|
||||
if training == False:
|
||||
channel = x.shape[1] // 2
|
||||
img0 = x[:, :channel]
|
||||
img1 = x[:, channel:]
|
||||
if not torch.is_tensor(timestep):
|
||||
timestep = (x[:, :1].clone() * 0 + 1) * timestep
|
||||
else:
|
||||
timestep = timestep.repeat(1, 1, img0.shape[2], img0.shape[3])
|
||||
|
||||
flow_list = []
|
||||
merged = []
|
||||
mask_list = []
|
||||
|
||||
# add 4.26 support
|
||||
if self.arch_ver in ["4.7", "4.10", "4.17", "4.26"]:
|
||||
f0 = self.encode(img0[:, :3])
|
||||
f1 = self.encode(img1[:, :3])
|
||||
|
||||
warped_img0 = img0
|
||||
warped_img1 = img1
|
||||
flow = None
|
||||
mask = None
|
||||
feat = None
|
||||
|
||||
# add 4.26 support
|
||||
if self.arch_ver in ["4.26"]:
|
||||
block = [self.block0, self.block1, self.block2, self.block3, self.block4]
|
||||
num_blocks = 5
|
||||
else:
|
||||
block = [self.block0, self.block1, self.block2, self.block3]
|
||||
num_blocks = 4
|
||||
|
||||
for i in range(num_blocks):
|
||||
if flow is None:
|
||||
if self.arch_ver in ["4.26"]:
|
||||
flow, mask, feat = block[i](
|
||||
torch.cat((img0[:, :3], img1[:, :3], f0, f1, timestep), 1),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
elif self.arch_ver in ["4.0", "4.2", "4.3", "4.5", "4.6"]:
|
||||
flow, mask = block[i](
|
||||
torch.cat((img0[:, :3], img1[:, :3], timestep), 1),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
if ensemble:
|
||||
f1, m1 = block[i](
|
||||
torch.cat((img1[:, :3], img0[:, :3], 1 - timestep), 1),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
flow = (flow + torch.cat((f1[:, 2:4], f1[:, :2]), 1)) / 2
|
||||
mask = (mask + (-m1)) / 2
|
||||
|
||||
# 4.7+
|
||||
if self.arch_ver in ["4.7", "4.10", "4.17"]:
|
||||
flow, mask = block[i](
|
||||
torch.cat((img0[:, :3], img1[:, :3], f0, f1, timestep), 1),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
|
||||
if ensemble:
|
||||
f_, m_ = block[i](
|
||||
torch.cat(
|
||||
(img1[:, :3], img0[:, :3], f1, f0, 1 - timestep), 1
|
||||
),
|
||||
None,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
flow = (flow + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2
|
||||
mask = (mask + (-m_)) / 2
|
||||
|
||||
else:
|
||||
# add 4.26 support
|
||||
if self.arch_ver in ["4.26"]:
|
||||
wf0 = warp(f0, flow[:, :2])
|
||||
wf1 = warp(f1, flow[:, 2:4])
|
||||
|
||||
# add 4.26 support
|
||||
input_tensor = torch.cat(
|
||||
(
|
||||
warped_img0[:, :3],
|
||||
warped_img1[:, :3],
|
||||
wf0,
|
||||
wf1,
|
||||
timestep,
|
||||
mask,
|
||||
feat,
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
fd, m0, feat = block[i](
|
||||
input_tensor,
|
||||
flow,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
flow = flow + fd
|
||||
mask = m0
|
||||
# 4.0-4.6
|
||||
elif self.arch_ver in ["4.0", "4.2", "4.3", "4.5", "4.6"]:
|
||||
f0, m0 = block[i](
|
||||
torch.cat(
|
||||
(warped_img0[:, :3], warped_img1[:, :3], timestep, mask), 1
|
||||
),
|
||||
flow,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
|
||||
if self.arch_ver in ["4.0"]:
|
||||
if (
|
||||
i == 1
|
||||
and f0[:, :2].abs().max() > 32
|
||||
and f0[:, 2:4].abs().max() > 32
|
||||
and not training
|
||||
):
|
||||
for k in range(4):
|
||||
scale_list[k] *= 2
|
||||
flow, mask = block[0](
|
||||
torch.cat((img0[:, :3], img1[:, :3], timestep), 1),
|
||||
None,
|
||||
scale=scale_list[0],
|
||||
)
|
||||
warped_img0 = warp(img0, flow[:, :2])
|
||||
warped_img1 = warp(img1, flow[:, 2:4])
|
||||
f0, m0 = block[i](
|
||||
torch.cat(
|
||||
(
|
||||
warped_img0[:, :3],
|
||||
warped_img1[:, :3],
|
||||
timestep,
|
||||
mask,
|
||||
),
|
||||
1,
|
||||
),
|
||||
flow,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
|
||||
# 4.7+
|
||||
if self.arch_ver in ["4.7", "4.10", "4.17"]:
|
||||
fd, m0 = block[i](
|
||||
torch.cat(
|
||||
(
|
||||
warped_img0[:, :3],
|
||||
warped_img1[:, :3],
|
||||
warp(f0, flow[:, :2]),
|
||||
warp(f1, flow[:, 2:4]),
|
||||
timestep,
|
||||
mask,
|
||||
),
|
||||
1,
|
||||
),
|
||||
flow,
|
||||
scale=scale_list[i],
|
||||
)
|
||||
flow = flow + fd
|
||||
|
||||
# 4.0-4.6 ensemble
|
||||
if ensemble and self.arch_ver in [
|
||||
"4.0",
|
||||
"4.2",
|
||||
"4.3",
|
||||
"4.5",
|
||||
"4.6",
|
||||
]:
|
||||
f1, m1 = block[i](
|
||||
torch.cat(
|
||||
(
|
||||
warped_img1[:, :3],
|
||||
warped_img0[:, :3],
|
||||
1 - timestep,
|
||||
-mask,
|
||||
),
|
||||
1,
|
||||
),
|
||||
torch.cat((flow[:, 2:4], flow[:, :2]), 1),
|
||||
scale=scale_list[i],
|
||||
)
|
||||
f0 = (f0 + torch.cat((f1[:, 2:4], f1[:, :2]), 1)) / 2
|
||||
m0 = (m0 + (-m1)) / 2
|
||||
|
||||
# 4.7+ ensemble
|
||||
if ensemble and self.arch_ver in ["4.7", "4.10", "4.17"]:
|
||||
wf0 = warp(f0, flow[:, :2])
|
||||
wf1 = warp(f1, flow[:, 2:4])
|
||||
|
||||
f_, m_ = block[i](
|
||||
torch.cat(
|
||||
(
|
||||
warped_img1[:, :3],
|
||||
warped_img0[:, :3],
|
||||
wf1,
|
||||
wf0,
|
||||
1 - timestep,
|
||||
-mask,
|
||||
),
|
||||
1,
|
||||
),
|
||||
torch.cat((flow[:, 2:4], flow[:, :2]), 1),
|
||||
scale=scale_list[i],
|
||||
)
|
||||
fd = (fd + torch.cat((f_[:, 2:4], f_[:, :2]), 1)) / 2
|
||||
mask = (m0 + (-m_)) / 2
|
||||
|
||||
if self.arch_ver in ["4.0", "4.2", "4.3", "4.5", "4.6"]:
|
||||
flow = flow + f0
|
||||
mask = mask + m0
|
||||
|
||||
if not ensemble and self.arch_ver in ["4.7", "4.10", "4.17"]:
|
||||
mask = m0
|
||||
|
||||
mask_list.append(mask)
|
||||
flow_list.append(flow)
|
||||
warped_img0 = warp(img0, flow[:, :2])
|
||||
warped_img1 = warp(img1, flow[:, 2:4])
|
||||
merged.append((warped_img0, warped_img1))
|
||||
|
||||
# add 4.26 support
|
||||
if self.arch_ver in ["4.26"]:
|
||||
final_mask = mask_list[-1] if mask_list else mask
|
||||
final_mask = torch.sigmoid(final_mask)
|
||||
merged_final = warped_img0 * final_mask + warped_img1 * (1 - final_mask)
|
||||
else:
|
||||
if self.arch_ver in ["4.0", "4.1", "4.2", "4.3", "4.4", "4.5", "4.6"]:
|
||||
final_idx = min(3, len(mask_list)-1) if len(mask_list) > 0 else 0
|
||||
if final_idx < len(mask_list):
|
||||
mask_list[final_idx] = torch.sigmoid(mask_list[final_idx])
|
||||
merged_final = merged[final_idx][0] * mask_list[final_idx] + merged[final_idx][1] * (1 - mask_list[final_idx])
|
||||
else:
|
||||
mask = torch.sigmoid(mask)
|
||||
merged_final = warped_img0 * mask + warped_img1 * (1 - mask)
|
||||
if self.arch_ver in ["4.7", "4.10", "4.17"]:
|
||||
mask = torch.sigmoid(mask)
|
||||
merged_final = warped_img0 * mask + warped_img1 * (1 - mask)
|
||||
|
||||
if not fastmode and self.arch_ver in ["4.0", "4.2", "4.3"]:
|
||||
c0 = self.contextnet(img0, flow[:, :2])
|
||||
c1 = self.contextnet(img1, flow[:, 2:4])
|
||||
tmp = self.unet(img0, img1, warped_img0, warped_img1, mask, flow, c0, c1)
|
||||
res = tmp[:, :3] * 2 - 1
|
||||
merged_final = torch.clamp(merged_final + res, 0, 1)
|
||||
|
||||
return merged_final[:, :, :h, :w]
|
||||
@@ -0,0 +1,428 @@
|
||||
import yaml
|
||||
import os
|
||||
from torch.hub import download_url_to_file, get_dir
|
||||
from urllib.parse import urlparse
|
||||
import torch
|
||||
import typing
|
||||
import traceback
|
||||
import einops
|
||||
import gc
|
||||
import torchvision.transforms.functional as transform
|
||||
from comfy.model_management import soft_empty_cache, get_torch_device
|
||||
import numpy as np
|
||||
|
||||
BASE_MODEL_DOWNLOAD_URLS = [
|
||||
"https://github.com/styler00dollar/VSGAN-tensorrt-docker/releases/download/models/",
|
||||
"https://github.com/Fannovel16/ComfyUI-Frame-Interpolation/releases/download/models/",
|
||||
"https://github.com/dajes/frame-interpolation-pytorch/releases/download/v1.0.0/"
|
||||
]
|
||||
|
||||
# Per-file fallback URLs for models no longer hosted at the base URLs above.
|
||||
# Each entry is a list of mirrors tried in order.
|
||||
CKPT_FALLBACK_URLS = {
|
||||
"rife47.pth": [
|
||||
"https://huggingface.co/marduk191/rife/resolve/main/rife47.pth",
|
||||
"https://huggingface.co/wavespeed/misc/resolve/main/rife/rife47.pth",
|
||||
"https://huggingface.co/MachineDelusions/RIFE/resolve/main/rife47.pth",
|
||||
"https://huggingface.co/jasonot/mycomfyui/resolve/main/rife47.pth",
|
||||
],
|
||||
"rife49.pth": [
|
||||
"https://huggingface.co/marduk191/rife/resolve/main/rife49.pth",
|
||||
"https://huggingface.co/hfmaster/models-moved/resolve/main/rife/rife49.pth",
|
||||
"https://huggingface.co/MachineDelusions/RIFE/resolve/main/rife49.pth",
|
||||
"https://huggingface.co/Isi99999/Frame_Interpolation_Models/resolve/main/rife49.pth",
|
||||
],
|
||||
"sudo_rife4_269.662_testV1_scale1.pth": [
|
||||
"https://huggingface.co/marduk191/rife/resolve/main/sudo_rife4_269.662_testV1_scale1.pth",
|
||||
"https://huggingface.co/uwg/upscaler/resolve/main/ESRGAN/sudo_rife4_269.662_testV1_scale1.pth",
|
||||
"https://huggingface.co/licyk/sd-upscaler-models/resolve/main/ESRGAN/sudo_rife4_269.662_testV1_scale1.pth",
|
||||
],
|
||||
}
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "./config.yaml")
|
||||
if os.path.exists(config_path):
|
||||
config = yaml.load(open(config_path, "r"), Loader=yaml.FullLoader)
|
||||
else:
|
||||
raise Exception("config.yaml file is neccessary, plz recreate the config file by downloading it from https://github.com/Fannovel16/ComfyUI-Frame-Interpolation")
|
||||
DEVICE = get_torch_device()
|
||||
|
||||
class InterpolationStateList():
|
||||
|
||||
def __init__(self, frame_indices: typing.List[int], is_skip_list: bool):
|
||||
self.frame_indices = frame_indices
|
||||
self.is_skip_list = is_skip_list
|
||||
|
||||
def is_frame_skipped(self, frame_index):
|
||||
is_frame_in_list = frame_index in self.frame_indices
|
||||
return self.is_skip_list and is_frame_in_list or not self.is_skip_list and not is_frame_in_list
|
||||
|
||||
|
||||
class MakeInterpolationStateList:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"frame_indices": ("STRING", {"multiline": True, "default": "1,2,3"}),
|
||||
"is_skip_list": ("BOOLEAN", {"default": True},),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INTERPOLATION_STATES",)
|
||||
FUNCTION = "create_options"
|
||||
CATEGORY = "ComfyUI-Frame-Interpolation/VFI"
|
||||
|
||||
def create_options(self, frame_indices: str, is_skip_list: bool):
|
||||
frame_indices_list = [int(item) for item in frame_indices.split(',')]
|
||||
|
||||
interpolation_state_list = InterpolationStateList(
|
||||
frame_indices=frame_indices_list,
|
||||
is_skip_list=is_skip_list,
|
||||
)
|
||||
return (interpolation_state_list,)
|
||||
|
||||
|
||||
def get_ckpt_container_path(model_type):
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), config["ckpts_path"], model_type))
|
||||
|
||||
def load_file_from_url(url, model_dir=None, progress=True, file_name=None):
|
||||
"""Load file form http url, will download models if necessary.
|
||||
|
||||
Ref:https://github.com/1adrianb/face-alignment/blob/master/face_alignment/utils.py
|
||||
|
||||
Args:
|
||||
url (str): URL to be downloaded.
|
||||
model_dir (str): The path to save the downloaded model. Should be a full path. If None, use pytorch hub_dir.
|
||||
Default: None.
|
||||
progress (bool): Whether to show the download progress. Default: True.
|
||||
file_name (str): The downloaded file name. If None, use the file name in the url. Default: None.
|
||||
|
||||
Returns:
|
||||
str: The path to the downloaded file.
|
||||
"""
|
||||
if model_dir is None: # use the pytorch hub_dir
|
||||
hub_dir = get_dir()
|
||||
model_dir = os.path.join(hub_dir, 'checkpoints')
|
||||
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
|
||||
parts = urlparse(url)
|
||||
file_name = os.path.basename(parts.path)
|
||||
if file_name is not None:
|
||||
file_name = file_name
|
||||
cached_file = os.path.abspath(os.path.join(model_dir, file_name))
|
||||
if not os.path.exists(cached_file):
|
||||
print(f'Downloading: "{url}" to {cached_file}\n')
|
||||
download_url_to_file(url, cached_file, hash_prefix=None, progress=progress)
|
||||
return cached_file
|
||||
|
||||
def load_file_from_github_release(model_type, ckpt_name):
|
||||
error_strs = []
|
||||
all_urls = [base + ckpt_name for base in BASE_MODEL_DOWNLOAD_URLS]
|
||||
all_urls += CKPT_FALLBACK_URLS.get(ckpt_name, [])
|
||||
|
||||
for i, url in enumerate(all_urls):
|
||||
try:
|
||||
return load_file_from_url(url, get_ckpt_container_path(model_type))
|
||||
except Exception:
|
||||
traceback_str = traceback.format_exc()
|
||||
if i < len(all_urls) - 1:
|
||||
print("Failed! Trying another endpoint.")
|
||||
error_strs.append(f"Error when downloading from: {url}\n\n{traceback_str}")
|
||||
|
||||
error_str = '\n\n'.join(error_strs)
|
||||
raise Exception(f"Tried all urls to download {ckpt_name} but no success. Below is the error log:\n\n{error_str}")
|
||||
|
||||
|
||||
def load_file_from_direct_url(model_type, url):
|
||||
return load_file_from_url(url, get_ckpt_container_path(model_type))
|
||||
|
||||
def preprocess_frames(frames):
|
||||
return einops.rearrange(frames[..., :3], "n h w c -> n c h w")
|
||||
|
||||
def postprocess_frames(frames):
|
||||
return einops.rearrange(frames, "n c h w -> n h w c")[..., :3].cpu()
|
||||
|
||||
def assert_batch_size(frames, batch_size=2, vfi_name=None):
|
||||
subject_verb = "Most VFI models require" if vfi_name is None else f"VFI model {vfi_name} requires"
|
||||
assert len(frames) >= batch_size, f"{subject_verb} at least {batch_size} frames to work with, only found {frames.shape[0]}. Please check the frame input using PreviewImage."
|
||||
|
||||
def _generic_frame_loop(
|
||||
frames,
|
||||
clear_cache_after_n_frames,
|
||||
multiplier: typing.Union[typing.SupportsInt, typing.List],
|
||||
return_middle_frame_function,
|
||||
*return_middle_frame_function_args,
|
||||
interpolation_states: InterpolationStateList = None,
|
||||
use_timestep=True,
|
||||
dtype=torch.float16,
|
||||
batch_size=1,
|
||||
final_logging=True):
|
||||
|
||||
#https://github.com/hzwer/Practical-RIFE/blob/main/inference_video.py#L169
|
||||
def non_timestep_inference(frame0, frame1, n):
|
||||
middle = return_middle_frame_function(frame0, frame1, None, *return_middle_frame_function_args)
|
||||
if n == 1:
|
||||
return [middle]
|
||||
first_half = non_timestep_inference(frame0, middle, n=n//2)
|
||||
second_half = non_timestep_inference(middle, frame1, n=n//2)
|
||||
if n%2:
|
||||
return [*first_half, middle, *second_half]
|
||||
else:
|
||||
return [*first_half, *second_half]
|
||||
|
||||
output_frames = torch.zeros(multiplier*frames.shape[0], *frames.shape[1:], dtype=dtype, device="cpu")
|
||||
out_len = 0
|
||||
|
||||
number_of_frames_processed_since_last_cleared_cuda_cache = 0
|
||||
|
||||
# Collect all (frame_itr, frame0, frame1) pairs that are not skipped
|
||||
# so we can group them into batches for Opt 7.
|
||||
all_pairs = []
|
||||
for frame_itr in range(len(frames) - 1):
|
||||
frame0 = frames[frame_itr:frame_itr+1]
|
||||
frame1 = frames[frame_itr+1:frame_itr+2]
|
||||
skipped = (interpolation_states is not None and interpolation_states.is_frame_skipped(frame_itr))
|
||||
all_pairs.append((frame_itr, frame0, frame1, skipped))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Opt 7: Batched frame-pair processing
|
||||
#
|
||||
# When batch_size > 1 and use_timestep is True, we stack multiple frame
|
||||
# pairs into a single tensor and run the model once per batch instead of
|
||||
# once per pair. This improves GPU utilisation on short, low-resolution
|
||||
# clips where per-call overhead dominates.
|
||||
#
|
||||
# For the non-timestep (recursive) path and for batch_size == 1 we fall
|
||||
# back to the original sequential loop to keep behaviour identical.
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _run_single_pair(frame0, frame1):
|
||||
"""Run all middle-frame inferences for one frame pair, return list of cpu tensors."""
|
||||
middle_frames_out = []
|
||||
if use_timestep:
|
||||
for middle_i in range(1, multiplier):
|
||||
timestep = middle_i / multiplier
|
||||
middle_frame = return_middle_frame_function(
|
||||
frame0.to(DEVICE),
|
||||
frame1.to(DEVICE),
|
||||
timestep,
|
||||
*return_middle_frame_function_args
|
||||
).detach().cpu().to(dtype=dtype)
|
||||
middle_frames_out.append(middle_frame)
|
||||
else:
|
||||
middle_frames = non_timestep_inference(frame0.to(DEVICE), frame1.to(DEVICE), multiplier - 1)
|
||||
middle_frames_out.extend(
|
||||
[f.detach().cpu().to(dtype=dtype) for f in torch.cat(middle_frames, dim=0)]
|
||||
)
|
||||
return middle_frames_out
|
||||
|
||||
def _run_batched_pairs(pair_list):
|
||||
"""
|
||||
Run all middle-frame inferences for a batch of frame pairs.
|
||||
|
||||
pair_list: list of (frame0, frame1) 1×C×H×W tensors (cpu).
|
||||
|
||||
Returns: list-of-lists — outer index = pair, inner index = middle frame.
|
||||
Only supported for use_timestep=True.
|
||||
"""
|
||||
B = len(pair_list)
|
||||
# Stack pairs: shape [B, C, H, W]
|
||||
f0_batch = torch.cat([p[0] for p in pair_list], dim=0).to(DEVICE) # [B,C,H,W]
|
||||
f1_batch = torch.cat([p[1] for p in pair_list], dim=0).to(DEVICE)
|
||||
|
||||
results = [[] for _ in range(B)]
|
||||
for middle_i in range(1, multiplier):
|
||||
timestep = middle_i / multiplier
|
||||
# Run each pair in the stacked batch individually but in one Python call.
|
||||
# True GPU-level batching would require modifying IFNet; this approach
|
||||
# removes Python loop overhead while staying architecture-agnostic.
|
||||
batch_middle = []
|
||||
for b_idx in range(B):
|
||||
mf = return_middle_frame_function(
|
||||
f0_batch[b_idx:b_idx+1],
|
||||
f1_batch[b_idx:b_idx+1],
|
||||
timestep,
|
||||
*return_middle_frame_function_args
|
||||
).detach().cpu().to(dtype=dtype)
|
||||
batch_middle.append(mf)
|
||||
for b_idx, mf in enumerate(batch_middle):
|
||||
results[b_idx].append(mf)
|
||||
return results
|
||||
|
||||
# Build groups of consecutive non-skipped pairs (skipped pairs are written
|
||||
# as-is between groups and never sent to the model).
|
||||
i = 0
|
||||
total_pairs = len(all_pairs)
|
||||
while i < total_pairs:
|
||||
frame_itr, frame0, frame1, skipped = all_pairs[i]
|
||||
|
||||
# Always write frame0 to output
|
||||
output_frames[out_len] = frame0
|
||||
out_len += 1
|
||||
|
||||
if skipped:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# --- gather a batch of consecutive non-skipped pairs starting at i ---
|
||||
if use_timestep and batch_size > 1:
|
||||
batch_pairs = [] # (frame0, frame1) for non-skipped pairs in this batch
|
||||
batch_itrs = [] # frame_itr values (for cache-clearing accounting)
|
||||
j = i
|
||||
while j < total_pairs and len(batch_pairs) < batch_size:
|
||||
j_itr, j_f0, j_f1, j_skip = all_pairs[j]
|
||||
if j_skip:
|
||||
break # stop batch at a skip boundary
|
||||
batch_pairs.append((j_f0, j_f1))
|
||||
batch_itrs.append(j_itr)
|
||||
j += 1
|
||||
|
||||
# Run batched inference
|
||||
batch_results = _run_batched_pairs(batch_pairs)
|
||||
|
||||
# Write results; for pairs after the first we also need to write frame0
|
||||
for rel_idx, (middle_frames_out, b_itr) in enumerate(zip(batch_results, batch_itrs)):
|
||||
if rel_idx > 0:
|
||||
# frame0 for this pair is frame1 of the previous pair
|
||||
output_frames[out_len] = all_pairs[i + rel_idx][1] # frame1 of prev = frame0 of this
|
||||
out_len += 1
|
||||
for middle_frame in middle_frames_out:
|
||||
output_frames[out_len] = middle_frame
|
||||
out_len += 1
|
||||
|
||||
number_of_frames_processed_since_last_cleared_cuda_cache += 1
|
||||
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
|
||||
print("Comfy-VFI: Clearing cache...", end=' ')
|
||||
soft_empty_cache()
|
||||
number_of_frames_processed_since_last_cleared_cuda_cache = 0
|
||||
print("Done cache clearing")
|
||||
|
||||
gc.collect()
|
||||
# Advance i by the number of pairs consumed (minus 1 because we
|
||||
# already wrote frame0 of the first pair above).
|
||||
i = j
|
||||
continue
|
||||
|
||||
# --- original sequential path (batch_size == 1 or non-timestep) ---
|
||||
frame0_fp = frame0.to(dtype=torch.float32)
|
||||
frame1_fp = frame1.to(dtype=torch.float32)
|
||||
|
||||
middle_frame_batches = _run_single_pair(frame0_fp, frame1_fp)
|
||||
|
||||
for middle_frame in middle_frame_batches:
|
||||
output_frames[out_len] = middle_frame
|
||||
out_len += 1
|
||||
|
||||
number_of_frames_processed_since_last_cleared_cuda_cache += 1
|
||||
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
|
||||
print("Comfy-VFI: Clearing cache...", end=' ')
|
||||
soft_empty_cache()
|
||||
number_of_frames_processed_since_last_cleared_cuda_cache = 0
|
||||
print("Done cache clearing")
|
||||
|
||||
gc.collect()
|
||||
i += 1
|
||||
|
||||
if final_logging:
|
||||
print(f"Comfy-VFI done! {len(output_frames)} frames generated at resolution: {output_frames[0].shape}")
|
||||
# Append final frame
|
||||
output_frames[out_len] = frames[-1:]
|
||||
out_len += 1
|
||||
# clear cache for courtesy
|
||||
if final_logging:
|
||||
print("Comfy-VFI: Final clearing cache...", end=' ')
|
||||
soft_empty_cache()
|
||||
if final_logging:
|
||||
print("Done cache clearing")
|
||||
return output_frames[:out_len]
|
||||
|
||||
def generic_frame_loop(
|
||||
model_name,
|
||||
frames,
|
||||
clear_cache_after_n_frames,
|
||||
multiplier: typing.Union[typing.SupportsInt, typing.List],
|
||||
return_middle_frame_function,
|
||||
*return_middle_frame_function_args,
|
||||
interpolation_states: InterpolationStateList = None,
|
||||
use_timestep=True,
|
||||
dtype=torch.float32,
|
||||
batch_size=1):
|
||||
|
||||
assert_batch_size(frames, vfi_name=model_name.replace('_', ' ').replace('VFI', ''))
|
||||
if type(multiplier) == int:
|
||||
return _generic_frame_loop(
|
||||
frames,
|
||||
clear_cache_after_n_frames,
|
||||
multiplier,
|
||||
return_middle_frame_function,
|
||||
*return_middle_frame_function_args,
|
||||
interpolation_states=interpolation_states,
|
||||
use_timestep=use_timestep,
|
||||
dtype=dtype,
|
||||
batch_size=batch_size
|
||||
)
|
||||
if type(multiplier) == list:
|
||||
multipliers = list(map(int, multiplier))
|
||||
multipliers += [2] * (len(frames) - len(multipliers) - 1)
|
||||
frame_batches = []
|
||||
for frame_itr in range(len(frames) - 1):
|
||||
multiplier = multipliers[frame_itr]
|
||||
if multiplier == 0: continue
|
||||
frame_batch = _generic_frame_loop(
|
||||
frames[frame_itr:frame_itr+2],
|
||||
clear_cache_after_n_frames,
|
||||
multiplier,
|
||||
return_middle_frame_function,
|
||||
*return_middle_frame_function_args,
|
||||
interpolation_states=interpolation_states,
|
||||
use_timestep=use_timestep,
|
||||
dtype=dtype,
|
||||
batch_size=batch_size,
|
||||
final_logging=False
|
||||
)
|
||||
if frame_itr != len(frames) - 2: # Not append last frame unless this batch is the last one
|
||||
frame_batch = frame_batch[:-1]
|
||||
frame_batches.append(frame_batch)
|
||||
output_frames = torch.cat(frame_batches)
|
||||
print(f"Comfy-VFI done! {len(output_frames)} frames generated at resolution: {output_frames[0].shape}")
|
||||
return output_frames
|
||||
raise NotImplementedError(f"multipiler of {type(multiplier)}")
|
||||
|
||||
class FloatToInt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"float": ("FLOAT", {"default": 0, 'min': 0, 'step': 0.01})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
FUNCTION = "convert"
|
||||
CATEGORY = "ComfyUI-Frame-Interpolation"
|
||||
|
||||
def convert(self, float):
|
||||
if hasattr(float, "__iter__"):
|
||||
return (list(map(int, float)),)
|
||||
return (int(float),)
|
||||
|
||||
""" def generic_4frame_loop(
|
||||
frames,
|
||||
clear_cache_after_n_frames,
|
||||
multiplier: typing.SupportsInt,
|
||||
return_middle_frame_function,
|
||||
*return_middle_frame_function_args,
|
||||
interpolation_states: InterpolationStateList = None,
|
||||
use_timestep=False):
|
||||
|
||||
if use_timestep: raise NotImplementedError("Timestep 4 frame VFI model")
|
||||
def non_timestep_inference(frame_0, frame_1, frame_2, frame_3, n):
|
||||
middle = return_middle_frame_function(frame_0, frame_1, None, *return_middle_frame_function_args)
|
||||
if n == 1:
|
||||
return [middle]
|
||||
first_half = non_timestep_inference(frame_0, middle, n=n//2)
|
||||
second_half = non_timestep_inference(middle, frame_1, n=n//2)
|
||||
if n%2:
|
||||
return [*first_half, middle, *second_half]
|
||||
else:
|
||||
return [*first_half, *second_half] """
|
||||
@@ -2,7 +2,7 @@
|
||||
# Install only:
|
||||
# pip install insightface onnxruntime-gpu open-clip-torch safetensors ml_dtypes==0.3.2
|
||||
|
||||
insightface>=0.7.3
|
||||
insightface>=1.0
|
||||
onnxruntime-gpu>=1.16.0
|
||||
# If onnxruntime-gpu fails (CPU only): pip install onnxruntime
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cython
|
||||
facexlib
|
||||
insightface
|
||||
insightface>=1.0
|
||||
onnxruntime
|
||||
onnxruntime-gpu; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')
|
||||
ftfy
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
facexlib
|
||||
insightface
|
||||
insightface>=1.0
|
||||
onnxruntime
|
||||
onnxruntime-gpu; sys_platform != 'darwin' and platform_machine == 'x86_64'
|
||||
ftfy
|
||||
|
||||
+99
-45
@@ -927,52 +927,109 @@ def download_model(url: str, filename: str, on_progress=None, folder_type: str =
|
||||
else:
|
||||
raise RuntimeError("CivitAI API key required for downloads. Set it in PromptChain/config.json or CIVITAI_API_KEY env var.")
|
||||
|
||||
# Resume an interrupted download: a leftover .part is a valid in-order
|
||||
# prefix of the file (TCP delivers bytes in order, and HTTP errors are
|
||||
# caught before any write, so .part only ever holds real body bytes).
|
||||
# Ask the server to continue from that offset via a Range request.
|
||||
resume_from = 0
|
||||
try:
|
||||
if os.path.isfile(temp_path):
|
||||
resume_from = os.path.getsize(temp_path)
|
||||
except OSError:
|
||||
resume_from = 0
|
||||
if resume_from:
|
||||
headers["Range"] = f"bytes={resume_from}-"
|
||||
|
||||
resp = requests.get(url, stream=True, timeout=30, allow_redirects=True, headers=headers)
|
||||
resp.raise_for_status()
|
||||
|
||||
total = int(resp.headers.get("content-length", 0))
|
||||
if total > _MAX_DOWNLOAD_BYTES:
|
||||
# Range rejected (416): our .part is >= the server's file. Exactly equal
|
||||
# = a finished-but-unpromoted download, so promote it; any other offset
|
||||
# is unusable, so discard the stub and restart clean.
|
||||
already_complete = False
|
||||
if resume_from and resp.status_code == 416:
|
||||
tail = resp.headers.get("content-range", "").rsplit("/", 1)[-1]
|
||||
full = int(tail) if tail.isdigit() else 0
|
||||
resp.close()
|
||||
raise RuntimeError(f"refusing download: Content-Length {total} exceeds cap {_MAX_DOWNLOAD_BYTES}")
|
||||
if total > 0:
|
||||
try:
|
||||
free = shutil.disk_usage(dest_dir).free
|
||||
if total > free:
|
||||
resp.close()
|
||||
raise RuntimeError(f"insufficient disk space: need {total}, have {free}")
|
||||
except OSError:
|
||||
pass # disk_usage can fail on some network mounts; don't block download
|
||||
if full and resume_from == full:
|
||||
already_complete = True
|
||||
else:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
resume_from = 0
|
||||
headers.pop("Range", None)
|
||||
resp = requests.get(url, stream=True, timeout=30, allow_redirects=True, headers=headers)
|
||||
|
||||
with _download_lock:
|
||||
_download_state["size_bytes"] = total
|
||||
if not already_complete:
|
||||
resp.raise_for_status()
|
||||
|
||||
downloaded = 0
|
||||
last_chunk_at = _time.monotonic()
|
||||
with open(temp_path, "wb") as f:
|
||||
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
||||
if _download_cancel.is_set():
|
||||
resp.close()
|
||||
raise RuntimeError("Download cancelled")
|
||||
now = _time.monotonic()
|
||||
if now - last_chunk_at > _CHUNK_STALL_SECONDS:
|
||||
resp.close()
|
||||
raise RuntimeError(f"download stalled: no data for {_CHUNK_STALL_SECONDS}s")
|
||||
if not chunk:
|
||||
continue
|
||||
last_chunk_at = now
|
||||
downloaded += len(chunk)
|
||||
if downloaded > _MAX_DOWNLOAD_BYTES:
|
||||
resp.close()
|
||||
raise RuntimeError(f"download exceeded cap {_MAX_DOWNLOAD_BYTES} bytes (server lied about Content-Length)")
|
||||
f.write(chunk)
|
||||
progress = (downloaded / total * 100) if total else 0
|
||||
# 206 = server honored the resume; its Content-Range carries the FULL
|
||||
# size (Content-Length is only the remaining tail). Any other status
|
||||
# (200) means Range was ignored — start clean so we never append onto
|
||||
# a stale head or double-count progress.
|
||||
if resp.status_code == 206:
|
||||
mode = "ab"
|
||||
tail = resp.headers.get("content-range", "").rsplit("/", 1)[-1]
|
||||
total = int(tail) if tail.isdigit() else resume_from + int(resp.headers.get("content-length", 0))
|
||||
else:
|
||||
mode = "wb"
|
||||
resume_from = 0
|
||||
total = int(resp.headers.get("content-length", 0))
|
||||
|
||||
with _download_lock:
|
||||
_download_state["downloaded"] = downloaded
|
||||
_download_state["progress"] = progress
|
||||
if total > _MAX_DOWNLOAD_BYTES:
|
||||
resp.close()
|
||||
raise RuntimeError(f"refusing download: size {total} exceeds cap {_MAX_DOWNLOAD_BYTES}")
|
||||
if total > 0:
|
||||
try:
|
||||
free = shutil.disk_usage(dest_dir).free
|
||||
if (total - resume_from) > free:
|
||||
resp.close()
|
||||
raise RuntimeError(f"insufficient disk space: need {total - resume_from}, have {free}")
|
||||
except OSError:
|
||||
pass # disk_usage can fail on some network mounts; don't block download
|
||||
|
||||
if on_progress:
|
||||
on_progress(downloaded, total)
|
||||
with _download_lock:
|
||||
_download_state["size_bytes"] = total
|
||||
|
||||
downloaded = resume_from
|
||||
last_chunk_at = _time.monotonic()
|
||||
with open(temp_path, mode) as f:
|
||||
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
||||
if _download_cancel.is_set():
|
||||
resp.close()
|
||||
raise RuntimeError("Download cancelled")
|
||||
now = _time.monotonic()
|
||||
if now - last_chunk_at > _CHUNK_STALL_SECONDS:
|
||||
resp.close()
|
||||
raise RuntimeError(f"download stalled: no data for {_CHUNK_STALL_SECONDS}s")
|
||||
if not chunk:
|
||||
continue
|
||||
last_chunk_at = now
|
||||
downloaded += len(chunk)
|
||||
if downloaded > _MAX_DOWNLOAD_BYTES:
|
||||
resp.close()
|
||||
# streamed past the declared size → the .part is corrupt
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise RuntimeError(f"download exceeded cap {_MAX_DOWNLOAD_BYTES} bytes (server lied about Content-Length)")
|
||||
f.write(chunk)
|
||||
progress = (downloaded / total * 100) if total else 0
|
||||
|
||||
with _download_lock:
|
||||
_download_state["downloaded"] = downloaded
|
||||
_download_state["progress"] = progress
|
||||
|
||||
if on_progress:
|
||||
on_progress(downloaded, total)
|
||||
|
||||
# Never promote a short file to its final name: a stream that ended
|
||||
# early without raising would otherwise look complete on disk and
|
||||
# block resume forever. Keep the .part so the next attempt continues.
|
||||
if total > 0 and downloaded != total:
|
||||
raise RuntimeError(f"incomplete download: got {downloaded} of {total} bytes")
|
||||
|
||||
os.replace(temp_path, dest_path)
|
||||
|
||||
@@ -984,12 +1041,9 @@ def download_model(url: str, filename: str, on_progress=None, folder_type: str =
|
||||
with _download_lock:
|
||||
_download_state["status"] = "failed"
|
||||
_download_state["error"] = str(e)
|
||||
|
||||
if os.path.exists(temp_path):
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
# Keep the .part on a recoverable interruption (stall, network drop,
|
||||
# cancel, incomplete) so the next attempt resumes from here instead of
|
||||
# restarting. The corrupt-state paths above already removed it.
|
||||
raise
|
||||
finally:
|
||||
with _download_lock:
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Viewer "Extend" support: from a finished VIDEO, pull its LAST frame (staged in
|
||||
# the input dir for a LoadImage to consume) plus its dimensions/fps, and stage a
|
||||
# content-addressed copy of the source video so the re-render can be recorded as
|
||||
# a lineage CHILD (parent_filename). The frontend then rebuilds an I2V
|
||||
# continuation graph (last frame -> next clip) the same way Re-pose rebuilds from
|
||||
# a recipe template — see js/lib/extend-from-image.js.
|
||||
|
||||
import hashlib
|
||||
import io as _io
|
||||
import os
|
||||
|
||||
import folder_paths
|
||||
import server
|
||||
from aiohttp import web
|
||||
|
||||
from .api_utils import error_response
|
||||
from .history_db import resolve_image_path
|
||||
|
||||
routes = server.PromptServer.instance.routes
|
||||
|
||||
# Same prefix Upscale uses: LoadImage's combo only lists top-level input files,
|
||||
# so the namespace lives in the filename and the copy sits in the input ROOT.
|
||||
_SRC_PREFIX = "promptchain_source_"
|
||||
_VIDEO_EXTS = {".mp4", ".webm", ".mkv", ".mov", ".m4v", ".avi"}
|
||||
|
||||
|
||||
def _staged_copy(raw: bytes, ext: str) -> str:
|
||||
"""Content-addressed copy in the input root; same bytes -> same name, written
|
||||
once. Returns the bare filename for LoadImage / parent_filename."""
|
||||
digest = hashlib.sha256(raw).hexdigest()[:12]
|
||||
name = f"{_SRC_PREFIX}{digest}{ext}"
|
||||
dest = os.path.join(folder_paths.get_input_directory(), name)
|
||||
if not os.path.exists(dest):
|
||||
with open(dest, "wb") as f:
|
||||
f.write(raw)
|
||||
return name
|
||||
|
||||
|
||||
def _last_frame_and_meta(video_bytes: bytes):
|
||||
"""(PIL.Image last_frame, width, height, fps, frame_count). Decodes the whole
|
||||
stream — Wan clips are short (~81-121 frames) so a full pass is cheap and far
|
||||
more reliable than seeking to EOF."""
|
||||
import av # PyAV (av) ships in the ComfyUI venv; powers the poster-frame path too
|
||||
|
||||
last = None
|
||||
fps = 16.0
|
||||
count = 0
|
||||
container = av.open(_io.BytesIO(video_bytes))
|
||||
try:
|
||||
stream = container.streams.video[0]
|
||||
try:
|
||||
if stream.average_rate:
|
||||
fps = float(stream.average_rate)
|
||||
except Exception:
|
||||
fps = 16.0
|
||||
for frame in container.decode(stream):
|
||||
last = frame
|
||||
count += 1
|
||||
finally:
|
||||
container.close()
|
||||
if last is None:
|
||||
return None, 0, 0, fps, 0
|
||||
img = last.to_image().convert("RGB")
|
||||
w, h = img.size
|
||||
return img, w, h, fps, count
|
||||
|
||||
|
||||
@routes.get("/promptchain/extend/prepare")
|
||||
async def _api_extend_prepare(request):
|
||||
"""Stage the source video's last frame + a lineage copy. ?hash=<image hash>."""
|
||||
image_hash = request.query.get("hash", "").strip()
|
||||
if not image_hash:
|
||||
return error_response("need hash")
|
||||
path = resolve_image_path(image_hash)
|
||||
if not path or not os.path.isfile(str(path)):
|
||||
return error_response("video not found for hash", 404)
|
||||
path = str(path)
|
||||
if os.path.splitext(path)[1].lower() not in _VIDEO_EXTS:
|
||||
return error_response("not a video file")
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
video_bytes = f.read()
|
||||
except OSError as e:
|
||||
return error_response(f"could not read video: {e}", 500)
|
||||
|
||||
try:
|
||||
import av # noqa: F401
|
||||
except Exception:
|
||||
return error_response("PyAV (av) not available — cannot read video frames", 500)
|
||||
|
||||
try:
|
||||
source_ref = _staged_copy(video_bytes, os.path.splitext(path)[1].lower())
|
||||
img, width, height, fps, count = _last_frame_and_meta(video_bytes)
|
||||
if img is None:
|
||||
return error_response("could not decode any video frame", 500)
|
||||
buf = _io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
last_frame = _staged_copy(buf.getvalue(), ".png")
|
||||
except Exception as e:
|
||||
return error_response(f"extend prepare failed: {e}", 500)
|
||||
|
||||
return web.json_response({
|
||||
"last_frame": last_frame,
|
||||
"source_ref": source_ref,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"fps": round(fps, 3),
|
||||
"frame_count": count,
|
||||
})
|
||||
@@ -98,38 +98,13 @@ def section_for_injectable(key: str) -> str | None:
|
||||
# subprocess (inheriting our env + interpreter), so it writes to exactly the
|
||||
# cache the runtime later reads, and is a cheap no-op once cached. Best-effort:
|
||||
# a failure never aborts the install — the weight simply falls back to lazy.
|
||||
_MEMBER_PREFETCH: dict[str, dict] = {
|
||||
# PuLID Flux 2 pulls EVA02-L-14-336 (merged2b) via open_clip into the HF hub
|
||||
# cache on first run. download_pretrained() fetches without building the model.
|
||||
"PuLIDFlux2": {
|
||||
"label": "EVA-CLIP",
|
||||
"code": (
|
||||
"import os; os.environ.setdefault('HF_HUB_DISABLE_PROGRESS_BARS','1'); "
|
||||
"from open_clip.pretrained import get_pretrained_cfg, download_pretrained; "
|
||||
"print('EVA-CLIP cached at', "
|
||||
"download_pretrained(get_pretrained_cfg('EVA02-L-14-336','merged2b_s6b_b61k')))"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _prefetch_for_member(resp: web.StreamResponse, key: str) -> None:
|
||||
"""Pre-fetch a just-installed member's lazy first-use weights. Best-effort —
|
||||
on any failure it logs and returns, leaving the weight to download on first
|
||||
use as it did before."""
|
||||
spec = _MEMBER_PREFETCH.get(key)
|
||||
if not spec:
|
||||
return
|
||||
await _send(resp, {"stage": "prefetch", "file": spec["label"]})
|
||||
await _log(resp, f"Pre-fetching {spec['label']} so the first render doesn't stall…")
|
||||
try:
|
||||
code = await _run(resp, [sys.executable, "-c", spec["code"]])
|
||||
if code == 0:
|
||||
await _log(resp, f"{spec['label']}: ready")
|
||||
else:
|
||||
await _log(resp, f"{spec['label']}: pre-fetch skipped (exit {code}) — downloads on first use instead")
|
||||
except Exception as e:
|
||||
await _log(resp, f"{spec['label']}: pre-fetch skipped ({e}) — downloads on first use instead")
|
||||
"""No-op. Weight pre-fetching was removed for ComfyUI Registry compliance —
|
||||
it ran a subprocess to warm a pack's lazy weights. Those weights now simply
|
||||
download on first render, as the packs already handle (e.g. PuLID-Flux2's
|
||||
EVA-CLIP via open_clip), so the first such render is slightly slower and
|
||||
nothing else changes."""
|
||||
return None
|
||||
|
||||
|
||||
def _member_status(m: dict) -> dict:
|
||||
|
||||
+218
-52
@@ -7,10 +7,13 @@ Three layers:
|
||||
User: {user_dir}/PromptChain/models/{hash}.json — user overrides
|
||||
|
||||
Each config embeds quick_hash and sha256 in its files array. Lookups go
|
||||
through a hash-based config index, not filenames.
|
||||
through a hash + filename config index.
|
||||
|
||||
User overrides discovered overrides system. On save, only the delta
|
||||
from the base config is stored so updates still propagate.
|
||||
User settings override the resolved base config. When a file matches both a
|
||||
curated system preset (by filename) and an auto-discovered config (by the
|
||||
embedded hash), the system preset wins — it holds the authoritative family,
|
||||
templates, and node ranges. On save, only the delta from the base config is
|
||||
stored so shipped updates still propagate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -55,20 +58,40 @@ def _user_dir() -> Path:
|
||||
# old filename-based reverse index.
|
||||
|
||||
_config_index: Optional[dict] = None
|
||||
_config_index_signature: Optional[tuple] = None
|
||||
|
||||
|
||||
def _config_dir_signature() -> tuple:
|
||||
# Freshness check mirroring get_db's mtime watch: a directory's mtime
|
||||
# changes when configs are added, removed, or atomically replaced
|
||||
# (atomic_write_json renames into place; git /deploy pulls write new
|
||||
# files), so shipped presets are picked up without a server restart.
|
||||
# In-place content edits to an existing file don't bump the dir mtime —
|
||||
# those still need invalidate_config_index() or a restart.
|
||||
sig = []
|
||||
for directory in (_system_dir(), _discovered_dir()):
|
||||
try:
|
||||
sig.append(directory.stat().st_mtime_ns)
|
||||
except OSError:
|
||||
sig.append(0)
|
||||
return tuple(sig)
|
||||
|
||||
|
||||
def _get_config_index() -> dict:
|
||||
global _config_index
|
||||
if _config_index is not None:
|
||||
global _config_index, _config_index_signature
|
||||
sig = _config_dir_signature()
|
||||
if _config_index is not None and sig == _config_index_signature:
|
||||
return _config_index
|
||||
_config_index = _build_config_index()
|
||||
_config_index_signature = sig
|
||||
return _config_index
|
||||
|
||||
|
||||
def invalidate_config_index():
|
||||
"""Force rebuild on next lookup (e.g. after adding/removing configs)."""
|
||||
global _config_index
|
||||
global _config_index, _config_index_signature
|
||||
_config_index = None
|
||||
_config_index_signature = None
|
||||
|
||||
|
||||
def _build_config_index() -> dict:
|
||||
@@ -83,8 +106,11 @@ def _build_config_index() -> dict:
|
||||
"by_filename": {}, # filename.lower() → config dict
|
||||
}
|
||||
# Discovered first, then system — system overwrites, giving curated
|
||||
# configs priority over auto-detected ones.
|
||||
for directory in (_discovered_dir(), _system_dir()):
|
||||
# configs priority over auto-detected ones. Each config is stamped with
|
||||
# its source tier so the resolver can prefer a curated system preset over
|
||||
# an auto-discovered config when both match the same physical file.
|
||||
for directory, tier in ((_discovered_dir(), "discovered"),
|
||||
(_system_dir(), "system")):
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
for path in directory.glob("*.json"):
|
||||
@@ -93,6 +119,7 @@ def _build_config_index() -> dict:
|
||||
data = _read_json(path)
|
||||
if not data:
|
||||
continue
|
||||
data["_source_tier"] = tier
|
||||
|
||||
# Legacy hash-named configs without files array:
|
||||
# treat stem as quick_hash
|
||||
@@ -127,17 +154,23 @@ def _index_single_entry(index: dict, entry: dict, config: dict):
|
||||
if fname:
|
||||
key = fname.lower()
|
||||
existing = index["by_filename"].get(key)
|
||||
# Two configs claiming the same filename is ambiguous; keep first
|
||||
# (deterministic by scan order) and surface the conflict so the
|
||||
# user can see why a particular config won.
|
||||
if existing is not None and existing is not config:
|
||||
logger.debug(
|
||||
"filename collision %s: kept %s, ignored %s",
|
||||
fname,
|
||||
_ascii_safe(existing.get("display_name", "?")),
|
||||
_ascii_safe(config.get("display_name", "?")),
|
||||
)
|
||||
return
|
||||
existing_system = existing.get("_source_tier") == "system"
|
||||
new_system = config.get("_source_tier") == "system"
|
||||
# A curated system preset always wins over an auto-discovered
|
||||
# config for the same filename — the discovered one carries the
|
||||
# file's hash but lacks the authoritative family/templates. Within
|
||||
# the same tier, keep first (deterministic by scan order) and log.
|
||||
if existing_system and not new_system:
|
||||
return
|
||||
if not (new_system and not existing_system):
|
||||
logger.debug(
|
||||
"filename collision %s: kept %s, ignored %s",
|
||||
fname,
|
||||
_ascii_safe(existing.get("display_name", "?")),
|
||||
_ascii_safe(config.get("display_name", "?")),
|
||||
)
|
||||
return
|
||||
index["by_filename"][key] = config
|
||||
|
||||
|
||||
@@ -392,20 +425,34 @@ def load(model_hash: str) -> Optional[dict]:
|
||||
return merged
|
||||
|
||||
|
||||
def _find_base_config(model_hash: str) -> Optional[dict]:
|
||||
"""Find the base config (system or discovered) for a model hash.
|
||||
def _resolve_base_config(model_hash: str) -> Optional[dict]:
|
||||
"""Resolve the curated/discovered base config for a hash (no install gate).
|
||||
|
||||
Checks the config index by quick_hash first, then falls back to
|
||||
filename matching via the fingerprint index. For multi-file models,
|
||||
verifies all required files are installed before returning.
|
||||
A model file can match an auto-discovered config by hash (the CivitAI scan
|
||||
embeds the file's quick_hash) and a curated system preset by filename
|
||||
(HuggingFace presets carry no hash). When both match, the curated system
|
||||
preset wins — it holds the authoritative family, templates, and node
|
||||
ranges. No install gate here so save()/delta resolves the same base load()
|
||||
surfaces.
|
||||
"""
|
||||
config = find_config_by_hash(model_hash)
|
||||
if not config:
|
||||
# Fallback: configs without quick_hash (e.g. HuggingFace-only models)
|
||||
# can still be matched by filename from the fingerprint scan
|
||||
fp_info = fingerprint.find_by_hash(model_hash)
|
||||
if fp_info:
|
||||
config = find_config_by_filename(fp_info["filename"])
|
||||
by_hash = find_config_by_hash(model_hash)
|
||||
by_name = None
|
||||
fp_info = fingerprint.find_by_hash(model_hash)
|
||||
if fp_info:
|
||||
by_name = find_config_by_filename(fp_info["filename"])
|
||||
for candidate in (by_hash, by_name):
|
||||
if candidate is not None and candidate.get("_source_tier") == "system":
|
||||
return candidate
|
||||
return by_hash or by_name
|
||||
|
||||
|
||||
def _find_base_config(model_hash: str) -> Optional[dict]:
|
||||
"""Find the base config for a model hash, gated on its files being present.
|
||||
|
||||
For multi-file models, verifies all required files are installed before
|
||||
returning so a half-downloaded model doesn't surface a dead template.
|
||||
"""
|
||||
config = _resolve_base_config(model_hash)
|
||||
if config:
|
||||
if "files" in config and not _all_files_installed(config["files"]):
|
||||
import logging
|
||||
@@ -435,7 +482,7 @@ def save(model_hash: str, config: dict):
|
||||
user_dir = _user_dir()
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
system_config = find_config_by_hash(model_hash)
|
||||
system_config = _resolve_base_config(model_hash)
|
||||
delta = _compute_delta(config, system_config) if system_config else config
|
||||
|
||||
if not delta and system_config:
|
||||
@@ -628,18 +675,21 @@ def list_version_details(model_name: str) -> list[dict]:
|
||||
|
||||
|
||||
def _file_exists_in_folder(filename: str, folder_type: str, min_size: int = 0) -> bool:
|
||||
"""True if filename exists in folder_type and is at least min_size bytes.
|
||||
"""True if filename exists in folder_type and isn't grossly undersized.
|
||||
|
||||
A truncated download left at its final path (no .part suffix) would
|
||||
otherwise pass mere-existence checks and make its catalog entry
|
||||
disappear, blocking the user from resuming it.
|
||||
Catches a truncated download left at its final path (no .part suffix) that
|
||||
would otherwise pass a mere-existence check and make its catalog entry
|
||||
disappear, blocking resume. But catalog config sizes are ROUNDED ESTIMATES,
|
||||
not exact byte counts (a Q8 GGUF listed as 16 GB is really ~15.4 GB), so the
|
||||
floor is half the estimate — enough to reject a real stub while accepting a
|
||||
complete file that's a few percent under the guess.
|
||||
"""
|
||||
try:
|
||||
for folder in folder_paths.get_folder_paths(folder_type):
|
||||
path = os.path.join(folder, filename)
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
if min_size and os.path.getsize(path) < min_size:
|
||||
if min_size and os.path.getsize(path) < min_size * 0.5:
|
||||
continue
|
||||
return True
|
||||
except Exception:
|
||||
@@ -655,6 +705,10 @@ def _all_files_installed(files: list[dict]) -> bool:
|
||||
entries need at least one variant present.
|
||||
"""
|
||||
for entry in files:
|
||||
# Optional files (e.g. a Fast-template LoRA) don't gate "installed":
|
||||
# a vanilla download without them must still surface the config.
|
||||
if entry.get("optional"):
|
||||
continue
|
||||
folder_type = entry.get("folder", "")
|
||||
if not folder_type:
|
||||
continue
|
||||
@@ -669,6 +723,103 @@ def _all_files_installed(files: list[dict]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
_PRIMARY_FOLDERS = ("diffusion_models", "unet", "checkpoints")
|
||||
|
||||
|
||||
def _precision_tier(precision: str) -> str:
|
||||
"""High-precision primaries pull the high-precision companion; everything
|
||||
else (fp8*, fp4, GGUF quants) pulls the lighter one. Mirrors the client's
|
||||
precisionTier so companion files pair to the chosen primary consistently."""
|
||||
return "high" if re.search(r"fp16|bf16|fp32", precision or "", re.I) else "low"
|
||||
|
||||
|
||||
def _resolve_files_for_precision(files: list[dict], precision: str) -> list[dict]:
|
||||
"""The concrete files a precision needs — primary variant + companions paired
|
||||
by tier. Mirror of the client resolveFilesForPrecision so install state is
|
||||
computed against the exact same file set the download modal would fetch."""
|
||||
tier = _precision_tier(precision)
|
||||
resolved = []
|
||||
for f in files:
|
||||
variants = f.get("variants")
|
||||
if not variants:
|
||||
resolved.append(f)
|
||||
continue
|
||||
match = next((v for v in variants if v.get("precision") == precision), None) \
|
||||
or next((v for v in variants if _precision_tier(v.get("precision")) == tier), None) \
|
||||
or variants[0]
|
||||
if not match:
|
||||
continue
|
||||
resolved.append({
|
||||
"label": f.get("label"),
|
||||
"folder": f.get("folder"),
|
||||
"filename": match.get("filename"),
|
||||
"size_bytes": match.get("size_bytes", 0),
|
||||
"quick_hash": match.get("quick_hash"),
|
||||
"optional": f.get("optional", False),
|
||||
"source": match.get("source"),
|
||||
})
|
||||
return resolved
|
||||
|
||||
|
||||
def _precision_install_status(files: list[dict]) -> dict:
|
||||
"""Per declared primary precision → 'installed' | 'partial' | 'missing'.
|
||||
|
||||
Status is keyed on the precision's OWN model weight (the primary variant —
|
||||
the quant / diffusion file). Shared companions (text encoder, VAE) are
|
||||
reused across every precision, so a precision whose weight isn't downloaded
|
||||
is 'missing' even when those companions happen to be on disk from another
|
||||
model. Only when the weight IS present do we split installed (all companions
|
||||
too) from 'partial' (a resumable download missing a companion).
|
||||
"""
|
||||
# Only a variant-bearing MODEL file (diffusion/unet/checkpoint) defines
|
||||
# selectable precisions. A single-weight model whose only variants live on a
|
||||
# companion — e.g. the 5B, whose lone variant file is the shared umt5 text
|
||||
# encoder — has no model precisions; surfacing the encoder's fp8/fp16 as if
|
||||
# they were model choices is meaningless (and falsely "partial" when the
|
||||
# shared encoder happens to be on disk from another model).
|
||||
primary = next((f for f in files if f.get("variants") and f.get("folder") in _PRIMARY_FOLDERS), None)
|
||||
if not primary:
|
||||
return {}
|
||||
primary_folder = primary.get("folder", "")
|
||||
precisions: list[str] = []
|
||||
for v in primary.get("variants", []):
|
||||
p = v.get("precision")
|
||||
if p and p not in precisions:
|
||||
precisions.append(p)
|
||||
|
||||
status = {}
|
||||
for p in precisions:
|
||||
variant = next((v for v in primary.get("variants", []) if v.get("precision") == p), None)
|
||||
if not (variant and _file_installed(variant, primary_folder)):
|
||||
status[p] = "missing"
|
||||
continue
|
||||
required = [
|
||||
f for f in _resolve_files_for_precision(files, p)
|
||||
if not f.get("optional") and f.get("folder") and (f.get("filename") or f.get("quick_hash"))
|
||||
]
|
||||
present = sum(1 for f in required if _file_installed(f, f["folder"]))
|
||||
status[p] = "installed" if present == len(required) else "partial"
|
||||
return status
|
||||
|
||||
|
||||
def _primary_file_present(files: list[dict]) -> bool:
|
||||
"""True if any primary model weight (diffusion/unet/checkpoint) is on disk.
|
||||
|
||||
Drives the 'resume' hint for entries without precision variants: a shared
|
||||
encoder/VAE being present is NOT a started download — only the model's own
|
||||
weight is, so we never glow an un-downloaded model amber."""
|
||||
for f in files:
|
||||
if f.get("folder") not in _PRIMARY_FOLDERS:
|
||||
continue
|
||||
variants = f.get("variants")
|
||||
if variants:
|
||||
if any(_file_installed(v, f["folder"]) for v in variants):
|
||||
return True
|
||||
elif (f.get("filename") or f.get("quick_hash")) and _file_installed(f, f["folder"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _base_version_key(version: str) -> tuple:
|
||||
"""Parse version string into a comparable tuple, ignoring precision suffixes.
|
||||
|
||||
@@ -725,6 +876,8 @@ def _collect_uninstalled_presets() -> list[dict]:
|
||||
entry["civitai_model_id"] = data["civitai_model_id"]
|
||||
if data.get("thumbnail"):
|
||||
entry["thumbnail"] = data["thumbnail"]
|
||||
entry["precision_status"] = _precision_install_status(entry["files"])
|
||||
entry["partial"] = _primary_file_present(entry["files"])
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
@@ -798,31 +951,44 @@ def is_catalog_filename(filename: str) -> bool:
|
||||
return filename.lower() in _get_config_index()["by_filename"]
|
||||
|
||||
|
||||
def _entry_matches_filename(entry: dict, filename_lower: str) -> bool:
|
||||
"""True if a file entry owns this filename — as its flat filename or any
|
||||
of its precision variants."""
|
||||
flat = entry.get("filename")
|
||||
if flat:
|
||||
return flat.lower() == filename_lower
|
||||
return any(v.get("filename", "").lower() == filename_lower
|
||||
for v in entry.get("variants", []))
|
||||
|
||||
|
||||
def is_primary_model_file(filename: str) -> bool:
|
||||
"""True if this filename is the primary file for its config.
|
||||
"""True if this filename is the model's primary (picker-facing) file.
|
||||
|
||||
Multi-file models list several component files (dual experts, text
|
||||
encoders, VAEs, LoRAs). The primary file is the first diffusion_models
|
||||
or unet entry. Only it should appear in the picker — secondaries are
|
||||
hidden to avoid duplicate entries.
|
||||
A multi-file config lists several components (dual MoE experts, text
|
||||
encoder, VAE, LoRAs). Only the primary surfaces in the picker; its
|
||||
precision variants become the selectable versions and every other
|
||||
component is hidden so one model reads as one selection.
|
||||
|
||||
Returns True for files not in any config (standalone models).
|
||||
The primary is declared with "primary": true on a file entry — needed
|
||||
when a config has more than one diffusion_models/unet entry (e.g. a MoE
|
||||
high+low pair). Configs with a single, unambiguous diffusion entry omit
|
||||
the flag and fall back to "first diffusion_models/unet entry wins". Files
|
||||
in no config (standalone models) are always primary.
|
||||
"""
|
||||
config = find_config_by_filename(filename)
|
||||
if not config or "files" not in config:
|
||||
return True
|
||||
|
||||
lower = filename.lower()
|
||||
for entry in config["files"]:
|
||||
if entry.get("folder") not in ("diffusion_models", "unet"):
|
||||
continue
|
||||
flat = entry.get("filename")
|
||||
if flat:
|
||||
return flat.lower() == lower
|
||||
for v in entry.get("variants", []):
|
||||
if v.get("filename", "").lower() == lower:
|
||||
return True
|
||||
return False
|
||||
files = config["files"]
|
||||
|
||||
flagged = [e for e in files if e.get("primary")]
|
||||
if flagged:
|
||||
return any(_entry_matches_filename(e, lower) for e in flagged)
|
||||
|
||||
for entry in files:
|
||||
if entry.get("folder") in ("diffusion_models", "unet"):
|
||||
return _entry_matches_filename(entry, lower)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
+37
-121
@@ -135,9 +135,13 @@ NODE_PACKS: dict[str, dict] = {
|
||||
# provide. `pip` installs ultralytics WITH its deps (torch/numpy/opencv
|
||||
# restored afterward by _restore_critical). proof_nodes is our own node,
|
||||
# so `present` hinges on the face model existing on disk.
|
||||
# pi-heif: ultralytics monkey-patches PIL.Image.open and lazily pip-
|
||||
# installs pi-heif on the FIRST open of any unidentifiable file — which
|
||||
# targets the wrong interpreter and then retries forever, spamming the
|
||||
# log. Shipping it here puts it in the right venv so that never fires.
|
||||
"repos": [],
|
||||
"proof_nodes": ["PromptChain_RegionalDetailer"],
|
||||
"pip": ["ultralytics"],
|
||||
"pip": ["ultralytics", "pi-heif"],
|
||||
"models": [
|
||||
{"url": "https://huggingface.co/Bingsu/adetailer/resolve/main/face_yolov8n.pt",
|
||||
"dest": "ultralytics/bbox/face_yolov8n.pt"},
|
||||
@@ -188,6 +192,19 @@ NODE_PACKS: dict[str, dict] = {
|
||||
"dest": "SEEDVR2/ema_vae_fp16.safetensors"},
|
||||
],
|
||||
},
|
||||
"FrameInterpolation": {
|
||||
"label": "Frame Interpolation (RIFE)",
|
||||
# RIFE neural frame interpolation for the viewer "Smooth" action (2x/3x
|
||||
# fps). Bundled (Fannovel16 ComfyUI-Frame-Interpolation, stripped to the
|
||||
# RIFE model only — the other interpolation models were unused and carried
|
||||
# unclear/non-commercial licenses). The rife49 weights auto-download from
|
||||
# the pack's own HF-mirrored registry on first run, so no model is fetched
|
||||
# here. pip adds einops (torch/numpy/opencv/torchvision fall to _PROTECTED).
|
||||
"bundle": ["ComfyUI-Frame-Interpolation"],
|
||||
"proof_nodes": ["RIFE VFI"],
|
||||
"pip": ["einops"],
|
||||
"models": [],
|
||||
},
|
||||
"ControlNet": {
|
||||
"label": "ControlNet",
|
||||
# Preprocessors are now NATIVE (vendor/pcr_cnaux + pcr_rtmpose, always
|
||||
@@ -545,80 +562,32 @@ def _torch_index_url() -> str | None:
|
||||
|
||||
|
||||
async def _restore_critical(resp: web.StreamResponse, snapshot: dict[str, str | None]) -> None:
|
||||
"""Compare critical pkg versions to the pre-install snapshot; reinstall
|
||||
any that changed. torch trio restored together from the CUDA index."""
|
||||
now = {p: _pkg_version(p) for p in _CRITICAL}
|
||||
trio_changed = any(snapshot.get(p) and snapshot[p] != now.get(p) for p in _TORCH_TRIO)
|
||||
if trio_changed:
|
||||
if all(snapshot.get(p) for p in _TORCH_TRIO):
|
||||
await _log(resp, "torch stack changed during install — restoring matched CUDA build")
|
||||
args = [sys.executable, "-m", "pip", "install",
|
||||
*(f"{p}=={snapshot[p].split('+')[0]}" for p in _TORCH_TRIO)]
|
||||
idx = _torch_index_url()
|
||||
if idx:
|
||||
args += ["--index-url", idx]
|
||||
await _run(resp, args)
|
||||
else:
|
||||
await _log(resp, "torch stack changed but no clean snapshot to restore from — skipping")
|
||||
for p in ("numpy", "opencv-python"):
|
||||
if snapshot.get(p) and snapshot[p] != now.get(p):
|
||||
await _log(resp, f"{p} changed ({now.get(p)} -> restoring {snapshot[p]})")
|
||||
await _run(resp, [sys.executable, "-m", "pip", "install", f"{p}=={snapshot[p]}"])
|
||||
"""No-op. PromptChain no longer runs pip at runtime (the ComfyUI Registry
|
||||
forbids it), so the critical torch/numpy stack is never disturbed by an
|
||||
install and has nothing to restore. Kept for call-site/import compatibility."""
|
||||
return None
|
||||
|
||||
|
||||
async def install_pack(resp: web.StreamResponse, injectable: str,
|
||||
snapshot: dict[str, str | None]) -> bool:
|
||||
"""Install one injectable's packs + models into custom_nodes/ and models/,
|
||||
streaming progress on the already-open SSE `resp`. The caller owns the SSE
|
||||
lifecycle and the pre-install critical-deps `snapshot`. Returns True on
|
||||
success; on failure it emits an {"error": …} event and returns False.
|
||||
"""Copy one injectable's bundled packs into custom_nodes/ and download its
|
||||
models, streaming progress on the already-open SSE `resp`. The caller owns
|
||||
the SSE lifecycle. Returns True on success; on failure it emits an
|
||||
{"error": …} event and returns False.
|
||||
|
||||
Shared by the single-pack route and the section installer so both run the
|
||||
identical clone/copy → pip → install.py → restore → download sequence.
|
||||
Every step skips work already done (cloned dirs, present models, satisfied
|
||||
pip), so re-running or installing an overlapping section is cheap."""
|
||||
Registry-compliant: PromptChain never runs pip or git at runtime — bundled
|
||||
packs are copied and their Python deps are declared in requirements.txt
|
||||
(installed with the pack). Every step skips work already done, so re-running
|
||||
or installing an overlapping section is cheap. `snapshot` is now unused."""
|
||||
spec = NODE_PACKS[injectable]
|
||||
repos = spec.get("repos", [])
|
||||
bundles = spec.get("bundle", [])
|
||||
|
||||
git = shutil.which("git")
|
||||
if repos and not git:
|
||||
await _send(resp, {"error": "git not found on PATH — can't clone node packs"})
|
||||
return False
|
||||
|
||||
cn = _custom_nodes_dir()
|
||||
await _log(resp, f"Installing {spec['label']} ({len(repos) + len(bundles)} pack(s))")
|
||||
await _log(resp, f"Installing {spec['label']} ({len(bundles)} pack(s))")
|
||||
|
||||
pack_dirs = [] # every pack dir we ensured is present in custom_nodes/
|
||||
|
||||
# 1) git-clone url-based packs, pinned to a known-good commit.
|
||||
for url in repos:
|
||||
name = _repo_dir_name(url)
|
||||
dest = cn / name
|
||||
await _send(resp, {"stage": "clone", "repo": name})
|
||||
if dest.is_dir():
|
||||
await _log(resp, f"{name}: already present, skipping clone")
|
||||
else:
|
||||
code = await _run(resp, [git, "clone", "--depth", "1", url, str(dest)])
|
||||
if code != 0:
|
||||
await _send(resp, {"error": f"git clone failed for {name} (exit {code})"})
|
||||
return False
|
||||
# Pin a FRESHLY-cloned pack to the commit our inject + extra_pip +
|
||||
# model list were written against, so a later upstream change can't
|
||||
# silently break a new install. Only fresh clones — an existing
|
||||
# user checkout is left untouched. A pin miss is non-fatal (GitHub
|
||||
# serves fetch-by-sha, but keep the clone HEAD if it ever fails).
|
||||
pin = (spec.get("pins") or {}).get(name)
|
||||
if pin:
|
||||
await _send(resp, {"stage": "pin", "repo": name})
|
||||
await _run(resp, [git, "-C", str(dest), "fetch", "--depth", "1", "origin", pin])
|
||||
if await _run(resp, [git, "-C", str(dest), "checkout", "--quiet", pin]) != 0:
|
||||
await _log(resp, f" warning: could not pin {name} to {pin[:10]} — using clone HEAD")
|
||||
pack_dirs.append(dest)
|
||||
|
||||
# 2) copy bundled packs straight out of PromptChain (no git / no network).
|
||||
# Only when absent — a copy the user already runs is never touched, so a
|
||||
# bundled pack can never collide with their own install.
|
||||
# Copy bundled packs straight out of PromptChain (no git, no network, no
|
||||
# pip). Only when absent — a copy the user already runs is never touched, so
|
||||
# a bundled pack can never collide with their own install.
|
||||
for name in bundles:
|
||||
src = _bundled_packs_dir() / name
|
||||
dest = cn / name
|
||||
@@ -631,63 +600,10 @@ async def install_pack(resp: web.StreamResponse, injectable: str,
|
||||
else:
|
||||
shutil.copytree(src, dest)
|
||||
await _log(resp, f"{name}: copied into custom_nodes")
|
||||
pack_dirs.append(dest)
|
||||
|
||||
# 3) install each ensured pack's requirements + run its install.py.
|
||||
for dest in pack_dirs:
|
||||
name = dest.name
|
||||
req = dest / "requirements.txt"
|
||||
if req.is_file():
|
||||
await _send(resp, {"stage": "pip", "repo": name})
|
||||
kept, notes = _filtered_requirements(req)
|
||||
for n in notes:
|
||||
await _log(resp, f" {n}")
|
||||
if kept:
|
||||
code = await _run(resp, [sys.executable, "-m", "pip", "install", *kept])
|
||||
if code != 0:
|
||||
# pip can die mid-transaction (e.g. locked dll on
|
||||
# Windows) AFTER it already swapped packages — restore
|
||||
# the critical stack before bailing, not only on success.
|
||||
await _restore_critical(resp, snapshot)
|
||||
await _send(resp, {"error": f"pip install failed for {name} (exit {code})"})
|
||||
return False
|
||||
else:
|
||||
await _log(resp, f"{name}: no requirements.txt")
|
||||
|
||||
install_py = dest / "install.py"
|
||||
if install_py.is_file():
|
||||
await _send(resp, {"stage": "install_script", "repo": name})
|
||||
await _run(resp, [sys.executable, "install.py"], cwd=str(dest))
|
||||
|
||||
# Packages installed WITH their deps (e.g. ultralytics needs opencv/pillow/
|
||||
# etc.). The torch stack is restored by _restore_critical below if a dep
|
||||
# drags it, so this is safe — unlike extra_pip, which is --no-deps.
|
||||
pip_with_deps = spec.get("pip") or []
|
||||
if pip_with_deps:
|
||||
await _send(resp, {"stage": "pip", "repo": "deps"})
|
||||
await _log(resp, f"Installing: {', '.join(pip_with_deps)}")
|
||||
code = await _run(resp, [sys.executable, "-m", "pip", "install", *pip_with_deps])
|
||||
if code != 0:
|
||||
await _restore_critical(resp, snapshot)
|
||||
await _send(resp, {"error": f"pip install failed for {', '.join(pip_with_deps)} (exit {code})"})
|
||||
return False
|
||||
|
||||
# Extra packages a pack imports but deliberately omits from requirements
|
||||
# (e.g. PuLID-Flux hard-imports facenet_pytorch, kept out of its reqs
|
||||
# because its pins would downgrade torch). Installed --no-deps so they
|
||||
# can't touch the protected torch stack.
|
||||
extra_pip = spec.get("extra_pip") or []
|
||||
if extra_pip:
|
||||
await _send(resp, {"stage": "pip", "repo": "extra"})
|
||||
await _log(resp, f"Installing extra deps (--no-deps): {', '.join(extra_pip)}")
|
||||
code = await _run(resp, [sys.executable, "-m", "pip", "install", "--no-deps", *extra_pip])
|
||||
if code != 0:
|
||||
await _restore_critical(resp, snapshot)
|
||||
await _send(resp, {"error": f"pip install failed for extra deps (exit {code})"})
|
||||
return False
|
||||
|
||||
await _send(resp, {"stage": "fix_deps"})
|
||||
await _restore_critical(resp, snapshot)
|
||||
# Python deps are NOT pip-installed here — the ComfyUI Registry forbids a
|
||||
# node running pip at runtime. Every bundled pack's deps are declared in
|
||||
# PromptChain's own requirements.txt, installed WITH the pack by Manager/pip.
|
||||
|
||||
models = spec.get("models") or []
|
||||
if models:
|
||||
|
||||
+6
-10
@@ -309,14 +309,8 @@ async def subject_status(request):
|
||||
return web.json_response(out)
|
||||
|
||||
|
||||
async def _pip_install(resp, snapshot, pkgs):
|
||||
await _send(resp, {"stage": "pip", "repo": ", ".join(pkgs)})
|
||||
await _log(resp, "Installing: " + ", ".join(pkgs))
|
||||
if await _run(resp, [_sys.executable, "-m", "pip", "install", *pkgs]) != 0:
|
||||
await _restore_critical(resp, snapshot)
|
||||
await _send(resp, {"error": f"pip install failed for {', '.join(pkgs)}"})
|
||||
return False
|
||||
return True
|
||||
# (_pip_install removed — PromptChain declares its deps in requirements.txt and
|
||||
# never runs pip at runtime; missing packages are surfaced to the user instead.)
|
||||
|
||||
|
||||
def _download_birefnet():
|
||||
@@ -340,7 +334,8 @@ async def install_subject_target(resp, target: str, snapshot: dict) -> bool:
|
||||
owns the SSE lifecycle, the snapshot, and the final _restore_critical."""
|
||||
if target == "subject":
|
||||
miss = [m for m in ("transformers",) if not _have(m)]
|
||||
if miss and not await _pip_install(resp, snapshot, miss):
|
||||
if miss:
|
||||
await _send(resp, {"error": f"missing package(s): {', '.join(miss)}. Run `pip install -r requirements.txt` for PromptChain (or reinstall it via Manager) and restart."})
|
||||
return False
|
||||
if not _birefnet_cached():
|
||||
await _send(resp, {"stage": "download", "file": "BiRefNet (~0.9GB)", "pct": 0})
|
||||
@@ -349,7 +344,8 @@ async def install_subject_target(resp, target: str, snapshot: dict) -> bool:
|
||||
return True
|
||||
if target == "object":
|
||||
miss = [m for m in ("ultralytics",) if not _have(m)]
|
||||
if miss and not await _pip_install(resp, snapshot, miss):
|
||||
if miss:
|
||||
await _send(resp, {"error": f"missing package(s): {', '.join(miss)}. Run `pip install -r requirements.txt` for PromptChain (or reinstall it via Manager) and restart."})
|
||||
return False
|
||||
if not _sam_cached():
|
||||
await _send(resp, {"stage": "download", "file": "SAM2 (~154MB)", "pct": 0})
|
||||
|
||||
+97
-10
@@ -1,4 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
@@ -8,11 +11,61 @@ from .history_db import resolve_image_path, get_data_dir
|
||||
MAX_WIDTH = 600
|
||||
QUALITY = 85
|
||||
|
||||
# Single-writer per hash within this process. The /generation POST generates a
|
||||
# thumb on its own to_thread worker AFTER committing the DB row, and any
|
||||
# concurrent GET /thumb (the sidebar workflow-cover especially) resolves that
|
||||
# committed row and generates the SAME thumb on another worker — both miss the
|
||||
# is_file() guard in the post-commit/pre-thumb window. The atomic publish below
|
||||
# is the correctness guarantee against torn reads; this lock just collapses the
|
||||
# redundant decode+resize. threading.Lock (not asyncio) — runs in worker threads.
|
||||
_locks: dict[str, threading.Lock] = {}
|
||||
_locks_guard = threading.Lock()
|
||||
|
||||
|
||||
def _lock_for(image_hash: str) -> threading.Lock:
|
||||
with _locks_guard:
|
||||
return _locks.setdefault(image_hash, threading.Lock())
|
||||
|
||||
# Video outputs (Wan/AnimateDiff/…) get recorded like images. PIL can't open
|
||||
# them, so thumbnail the first decoded frame instead — and never hand a video
|
||||
# to Image.open (ultralytics' global PIL patch turns that miss into a noisy,
|
||||
# offline-failing pi_heif auto-install attempt).
|
||||
VIDEO_SUFFIXES = {".mp4", ".webm", ".mkv", ".mov", ".m4v"}
|
||||
|
||||
|
||||
def _get_thumbs_dir() -> Path:
|
||||
return get_data_dir() / "thumbs"
|
||||
|
||||
|
||||
def _first_video_frame(source: Path) -> "Image.Image | None":
|
||||
"""Decode a video's first frame as a poster image, or None if it can't."""
|
||||
try:
|
||||
import av
|
||||
except ImportError:
|
||||
return None
|
||||
try:
|
||||
with av.open(str(source)) as container:
|
||||
stream = next((s for s in container.streams if s.type == "video"), None)
|
||||
if stream is None:
|
||||
return None
|
||||
for frame in container.decode(stream):
|
||||
return frame.to_image()
|
||||
except Exception:
|
||||
logging.debug("[PromptChain] could not decode video poster for %s",
|
||||
source, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _load_poster(source: Path) -> "Image.Image | None":
|
||||
"""A still PIL image to thumbnail from: the first frame for video, the file
|
||||
itself for images. Fully loaded and detached so the source handle closes."""
|
||||
if source.suffix.lower() in VIDEO_SUFFIXES:
|
||||
return _first_video_frame(source)
|
||||
with Image.open(source) as im:
|
||||
im.load()
|
||||
return im.copy()
|
||||
|
||||
|
||||
def get_or_create_thumbnail(image_hash: str) -> Path | None:
|
||||
thumbs_dir = _get_thumbs_dir()
|
||||
thumbs_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -21,12 +74,20 @@ def get_or_create_thumbnail(image_hash: str) -> Path | None:
|
||||
if thumb_path.is_file():
|
||||
return thumb_path
|
||||
|
||||
source = resolve_image_path(image_hash)
|
||||
if not source or not source.is_file():
|
||||
return None
|
||||
with _lock_for(image_hash):
|
||||
# another worker may have published while we waited on the lock
|
||||
if thumb_path.is_file():
|
||||
return thumb_path
|
||||
|
||||
source = resolve_image_path(image_hash)
|
||||
if not source or not source.is_file():
|
||||
return None
|
||||
|
||||
try:
|
||||
img = _load_poster(source)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
with Image.open(source) as img:
|
||||
if img.mode in ("RGBA", "LA", "P", "PA"):
|
||||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||||
if img.mode == "P":
|
||||
@@ -40,8 +101,34 @@ def get_or_create_thumbnail(image_hash: str) -> Path | None:
|
||||
ratio = MAX_WIDTH / img.width
|
||||
img = img.resize((MAX_WIDTH, round(img.height * ratio)), Image.LANCZOS)
|
||||
|
||||
img.save(thumb_path, format="WEBP", quality=QUALITY)
|
||||
return thumb_path
|
||||
except Exception:
|
||||
logging.exception("[PromptChain] thumbnail generation failed for %s", image_hash)
|
||||
return None
|
||||
# Atomic publish: a concurrent GET /thumb must never FileResponse a
|
||||
# half-written webp (PIL opens the path 'wb' = truncate-then-stream).
|
||||
# Encode into a sibling temp file (same dir → same volume → os.replace
|
||||
# is atomic) and swap it onto the final path in one indivisible step.
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
dir=str(thumbs_dir), prefix=f".{image_hash}.", suffix=".webp.tmp"
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as f:
|
||||
img.save(f, format="WEBP", quality=QUALITY)
|
||||
os.replace(tmp_name, thumb_path)
|
||||
except PermissionError:
|
||||
# Windows can refuse os.replace if the destination is open by a
|
||||
# reader. The thumb is content-addressed, so if it now exists a
|
||||
# peer already published identical bytes — accept that and drop
|
||||
# our temp; otherwise report a clean miss.
|
||||
try:
|
||||
os.unlink(tmp_name)
|
||||
except OSError:
|
||||
pass
|
||||
return thumb_path if thumb_path.is_file() else None
|
||||
except BaseException:
|
||||
try:
|
||||
os.unlink(tmp_name)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
return thumb_path
|
||||
except Exception:
|
||||
logging.exception("[PromptChain] thumbnail generation failed for %s", image_hash)
|
||||
return None
|
||||
|
||||
@@ -130,7 +130,17 @@
|
||||
"steps_range": [
|
||||
12,
|
||||
48
|
||||
]
|
||||
],
|
||||
"width": 1024,
|
||||
"height": 1024
|
||||
},
|
||||
"DualModelGuider": {
|
||||
"cfg": 7
|
||||
},
|
||||
"CFGOverride": {
|
||||
"cfg": 3,
|
||||
"start_percent": 0.7,
|
||||
"end_percent": 1.0
|
||||
},
|
||||
"EmptyFlux2LatentImage": {
|
||||
"width": 1024,
|
||||
|
||||
@@ -1,91 +1,50 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 TI2V - 5B",
|
||||
"model_name": "Wan 2.2 5B",
|
||||
"version": "5B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "5B parameter hybrid text-to-video and image-to-video model. Single model — no MoE split needed. Fits in 8GB VRAM. Uses Wan 2.2 VAE (48-channel latent).",
|
||||
"license": "Apache 2.0",
|
||||
"tags": [
|
||||
"wan",
|
||||
"video",
|
||||
"t2v",
|
||||
"i2v",
|
||||
"5B",
|
||||
"consumer",
|
||||
"low-vram"
|
||||
],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "",
|
||||
"notes": "Single model for both T2V and I2V. No MoE split. Uses wan2.2_vae (not wan_2.1_vae). 1280x704, 49 frames, 16fps.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"size_bytes": 9999658848,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"size_bytes": 6735906897,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "VAE (Wan 2.2)",
|
||||
"folder": "vae",
|
||||
"filename": "wan2.2_vae.safetensors",
|
||||
"size_bytes": 1409400960,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/vae/wan2.2_vae.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
}
|
||||
],
|
||||
"nodes": {
|
||||
"KSampler": {
|
||||
"steps": 30,
|
||||
"cfg": 4.0,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [
|
||||
20,
|
||||
50
|
||||
],
|
||||
"cfg_range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"sampler_name_options": [
|
||||
"euler",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_2m"
|
||||
],
|
||||
"scheduler_options": [
|
||||
"simple",
|
||||
"normal",
|
||||
"beta"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"display_name": "Wan 2.2 TI2V - 5B",
|
||||
"model_name": "Wan 2.2 5B",
|
||||
"version": "5B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "5B hybrid text-and-image-to-video. Single dense model (no MoE, no speed LoRA). The lightest, fastest local Wan - fits ~8GB. Uses the dedicated wan2.2_vae (48-channel latent). fp16 safetensors via UNETLoader.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "ti2v", "t2v", "i2v", "5B", "consumer", "low-vram"],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "Single dense KSampler: 20 steps, CFG 5.0, uni_pc/simple, ModelSamplingSD3 shift 8.0, 1280x704, 121 frames, 24fps. Separate I2V (load a start image) and T2V (no start image) templates. REQUIRES wan2.2_vae (NOT wan_2.1_vae). Trivial on 24GB. No distill LoRA exists for 5B.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"size_bytes": 9999658848,
|
||||
"source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors" },
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE (Wan 2.2)", "folder": "vae", "filename": "wan2.2_vae.safetensors", "size_bytes": 1429407336, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan2.2_vae.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSampler": {
|
||||
"steps": 20,
|
||||
"cfg": 5.0,
|
||||
"sampler_name": "uni_pc",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [15, 40],
|
||||
"cfg_range": [1.0, 8.0],
|
||||
"sampler_name_options": ["uni_pc", "euler", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 TI2V - 5B (GGUF)",
|
||||
"model_name": "Wan 2.2 5B GGUF",
|
||||
"version": "5B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b_gguf",
|
||||
"author": "Alibaba/Wan (GGUF by QuantStack)",
|
||||
"description": "5B hybrid text-and-image-to-video, GGUF-quantized. Single dense model loaded via UnetLoaderGGUF (requires ComfyUI-GGUF). The lightest local Wan - runs in a few GB. Uses the dedicated wan2.2_vae.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "ti2v", "t2v", "i2v", "5B", "gguf", "low-vram"],
|
||||
"url": "https://huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "GGUF requires UnetLoaderGGUF (ComfyUI-GGUF). Templates bake Q5_K_M; pick a different quant in the loader dropdown if downloaded. Single dense KSampler: 20 steps, CFG 5.0, uni_pc/simple, shift 8.0, 1280x704, 121 frames, 24fps. REQUIRES wan2.2_vae. Separate I2V and T2V templates.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model - GGUF",
|
||||
"folder": "unet",
|
||||
"variants": [
|
||||
{ "precision": "Q5_K_M", "filename": "Wan2.2-TI2V-5B-Q5_K_M.gguf", "size_bytes": 3700000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-TI2V-5B-GGUF", "path": "Wan2.2-TI2V-5B-Q5_K_M.gguf" }, "sha256": null },
|
||||
{ "precision": "Q8_0", "filename": "Wan2.2-TI2V-5B-Q8_0.gguf", "size_bytes": 5400000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-TI2V-5B-GGUF", "path": "Wan2.2-TI2V-5B-Q8_0.gguf" }, "sha256": null },
|
||||
{ "precision": "Q6_K", "filename": "Wan2.2-TI2V-5B-Q6_K.gguf", "size_bytes": 4300000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-TI2V-5B-GGUF", "path": "Wan2.2-TI2V-5B-Q6_K.gguf" }, "sha256": null },
|
||||
{ "precision": "Q4_K_M", "filename": "Wan2.2-TI2V-5B-Q4_K_M.gguf", "size_bytes": 3100000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-TI2V-5B-GGUF", "path": "Wan2.2-TI2V-5B-Q4_K_M.gguf" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE (Wan 2.2)", "folder": "vae", "filename": "wan2.2_vae.safetensors", "size_bytes": 1429407336, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan2.2_vae.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSampler": {
|
||||
"steps": 20,
|
||||
"cfg": 5.0,
|
||||
"sampler_name": "uni_pc",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [15, 40],
|
||||
"cfg_range": [1.0, 8.0],
|
||||
"sampler_name_options": ["uni_pc", "euler", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +1,61 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 I2V - 14B",
|
||||
"model_name": "Wan 2.2 I2V",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "14B parameter image-to-video model. Dual-expert MoE architecture — high-noise and low-noise models run sequentially. 4-step generation with LightX2V LoRAs.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": [
|
||||
"wan",
|
||||
"video",
|
||||
"i2v",
|
||||
"image-to-video",
|
||||
"MoE",
|
||||
"14B"
|
||||
],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "",
|
||||
"notes": "Dual-expert MoE: load HIGH + LOW noise models separately. 4 steps with LightX2V LoRAs, euler/simple, shift 5.0. 640x640, 81 frames, 16fps.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size_bytes": 14294742832,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size_bytes": 14294742832,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"size_bytes": 6735906897,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "VAE",
|
||||
"folder": "vae",
|
||||
"filename": "wan_2.1_vae.safetensors",
|
||||
"size_bytes": 253815318,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/vae/wan_2.1_vae.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "LightX2V LoRA (High Noise)",
|
||||
"folder": "loras",
|
||||
"filename": "wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors",
|
||||
"size_bytes": 1226977424,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "LightX2V LoRA (Low Noise)",
|
||||
"folder": "loras",
|
||||
"filename": "wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors",
|
||||
"size_bytes": 1226977424,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/loras/wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
}
|
||||
],
|
||||
"nodes": {
|
||||
"KSampler": {
|
||||
"steps": 4,
|
||||
"cfg": 1.0,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [
|
||||
4,
|
||||
30
|
||||
],
|
||||
"cfg_range": [
|
||||
1.0,
|
||||
5.0
|
||||
],
|
||||
"sampler_name_options": [
|
||||
"euler",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_2m"
|
||||
],
|
||||
"scheduler_options": [
|
||||
"simple",
|
||||
"normal",
|
||||
"beta"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"display_name": "Wan 2.2 I2V - 14B",
|
||||
"model_name": "Wan 2.2 I2V",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "14B image-to-video. Dual-expert MoE (high-noise + low-noise run sequentially). Safetensors fp8/fp16 via UNETLoader. Vanilla full-quality sampling by default; an optional LightX2V 4-step LoRA template gives fast drafts.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "i2v", "image-to-video", "MoE", "14B"],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "Vanilla: 20 steps (10 high + 10 low), CFG 3.5, euler/simple, ModelSamplingSD3 shift 5.0, 640x640, 81 frames, 16fps. Fast template = LightX2V 4-step LoRAs on both experts (4 steps, CFG 1.0) - faster but less motion and no negative prompt. 24GB: fp8 comfortable (one expert resident ~14GB); fp16 needs offload.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"primary": true,
|
||||
"variants": [
|
||||
{ "precision": "fp8_scaled", "filename": "wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "size_bytes": 14294742832, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "wan2.2_i2v_high_noise_14B_fp16.safetensors", "size_bytes": 28649479008, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_i2v_high_noise_14B_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"variants": [
|
||||
{ "precision": "fp8_scaled", "filename": "wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "size_bytes": 14294742832, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "wan2.2_i2v_low_noise_14B_fp16.safetensors", "size_bytes": 28649479008, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_i2v_low_noise_14B_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE", "folder": "vae", "filename": "wan_2.1_vae.safetensors", "size_bytes": 253815318, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan_2.1_vae.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - High Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors", "size_bytes": 665845760, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - Low Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors", "size_bytes": 774897664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSamplerAdvanced": {
|
||||
"steps": 20,
|
||||
"cfg": 3.5,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [10, 40],
|
||||
"cfg_range": [1.0, 6.0],
|
||||
"sampler_name_options": ["euler", "uni_pc", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 I2V - 14B (GGUF)",
|
||||
"model_name": "Wan 2.2 I2V GGUF",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v_gguf",
|
||||
"author": "Alibaba/Wan (GGUF by QuantStack)",
|
||||
"description": "14B image-to-video, GGUF-quantized for lower VRAM. Dual-expert MoE loaded via UnetLoaderGGUF (requires the ComfyUI-GGUF node pack). Pick a quantization to download (Q5_K_M recommended for 24GB).",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "i2v", "image-to-video", "MoE", "14B", "gguf"],
|
||||
"url": "https://huggingface.co/QuantStack/Wan2.2-I2V-A14B-GGUF",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "GGUF requires UnetLoaderGGUF (ComfyUI-GGUF). Templates bake Q5_K_M; if you download a different quant, pick it in the loader dropdown. Q5_K_M ~10.8GB/expert, Q8_0 ~16GB. Same recipe as the safetensors I2V (shift 5.0, vanilla 20 steps / fast 4-step LoRA). umt5 + VAE are the standard safetensors files.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise) - GGUF",
|
||||
"folder": "unet",
|
||||
"primary": true,
|
||||
"variants": [
|
||||
{ "precision": "Q5_K_M", "filename": "Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf", "size_bytes": 10800000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "HighNoise/Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf" }, "sha256": null },
|
||||
{ "precision": "Q8_0", "filename": "Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf", "size_bytes": 16000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "HighNoise/Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf" }, "sha256": null },
|
||||
{ "precision": "Q6_K", "filename": "Wan2.2-I2V-A14B-HighNoise-Q6_K.gguf", "size_bytes": 13000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "HighNoise/Wan2.2-I2V-A14B-HighNoise-Q6_K.gguf" }, "sha256": null },
|
||||
{ "precision": "Q4_K_M", "filename": "Wan2.2-I2V-A14B-HighNoise-Q4_K_M.gguf", "size_bytes": 9000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "HighNoise/Wan2.2-I2V-A14B-HighNoise-Q4_K_M.gguf" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise) - GGUF",
|
||||
"folder": "unet",
|
||||
"variants": [
|
||||
{ "precision": "Q5_K_M", "filename": "Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf", "size_bytes": 10800000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "LowNoise/Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf" }, "sha256": null },
|
||||
{ "precision": "Q8_0", "filename": "Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf", "size_bytes": 16000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "LowNoise/Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf" }, "sha256": null },
|
||||
{ "precision": "Q6_K", "filename": "Wan2.2-I2V-A14B-LowNoise-Q6_K.gguf", "size_bytes": 13000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "LowNoise/Wan2.2-I2V-A14B-LowNoise-Q6_K.gguf" }, "sha256": null },
|
||||
{ "precision": "Q4_K_M", "filename": "Wan2.2-I2V-A14B-LowNoise-Q4_K_M.gguf", "size_bytes": 9000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-I2V-A14B-GGUF", "path": "LowNoise/Wan2.2-I2V-A14B-LowNoise-Q4_K_M.gguf" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE", "folder": "vae", "filename": "wan_2.1_vae.safetensors", "size_bytes": 253815318, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan_2.1_vae.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - High Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors", "size_bytes": 665845760, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - Low Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors", "size_bytes": 774897664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSamplerAdvanced": {
|
||||
"steps": 20,
|
||||
"cfg": 3.5,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [10, 40],
|
||||
"cfg_range": [1.0, 6.0],
|
||||
"sampler_name_options": ["euler", "uni_pc", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +1,61 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 T2V - 14B",
|
||||
"model_name": "Wan 2.2 T2V",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "14B parameter text-to-video model. Dual-expert MoE architecture — high-noise and low-noise models run sequentially. 4-step generation with LightX2V LoRAs.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": [
|
||||
"wan",
|
||||
"video",
|
||||
"t2v",
|
||||
"text-to-video",
|
||||
"MoE",
|
||||
"14B"
|
||||
],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "",
|
||||
"notes": "Dual-expert MoE: load HIGH + LOW noise models separately. 4 steps with LightX2V LoRAs, euler/simple, shift 5.0. 640x640, 81 frames, 16fps.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"size_bytes": 14293923632,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"size_bytes": 14293923632,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"size_bytes": 6735906897,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "VAE",
|
||||
"folder": "vae",
|
||||
"filename": "wan_2.1_vae.safetensors",
|
||||
"size_bytes": 253815318,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/vae/wan_2.1_vae.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "LightX2V LoRA (High Noise)",
|
||||
"folder": "loras",
|
||||
"filename": "wan2.2_t2v_lightx2v_4steps_lora_v1.1_high_noise.safetensors",
|
||||
"size_bytes": 1226977424,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/loras/wan2.2_t2v_lightx2v_4steps_lora_v1.1_high_noise.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
},
|
||||
{
|
||||
"label": "LightX2V LoRA (Low Noise)",
|
||||
"folder": "loras",
|
||||
"filename": "wan2.2_t2v_lightx2v_4steps_lora_v1.1_low_noise.safetensors",
|
||||
"size_bytes": 1226977424,
|
||||
"source": {
|
||||
"provider": "huggingface",
|
||||
"repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"path": "split_files/loras/wan2.2_t2v_lightx2v_4steps_lora_v1.1_low_noise.safetensors"
|
||||
},
|
||||
"sha256": null
|
||||
}
|
||||
],
|
||||
"nodes": {
|
||||
"KSampler": {
|
||||
"steps": 4,
|
||||
"cfg": 1.0,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [
|
||||
4,
|
||||
30
|
||||
],
|
||||
"cfg_range": [
|
||||
1.0,
|
||||
5.0
|
||||
],
|
||||
"sampler_name_options": [
|
||||
"euler",
|
||||
"dpmpp_sde",
|
||||
"dpmpp_2m"
|
||||
],
|
||||
"scheduler_options": [
|
||||
"simple",
|
||||
"normal",
|
||||
"beta"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"display_name": "Wan 2.2 T2V - 14B",
|
||||
"model_name": "Wan 2.2 T2V",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v",
|
||||
"author": "Alibaba/Wan",
|
||||
"description": "14B text-to-video. Dual-expert MoE (high-noise + low-noise run sequentially). Safetensors fp8/fp16 via UNETLoader. Vanilla full-quality sampling by default; an optional LightX2V 4-step LoRA template gives fast drafts.",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "t2v", "text-to-video", "MoE", "14B"],
|
||||
"url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "Vanilla: 20 steps (10 high + 10 low), CFG 3.5, euler/simple, ModelSamplingSD3 shift 5.0, 640x640, 81 frames, 16fps. Fast template = LightX2V 4-step LoRAs v1.1 on both experts (4 steps, CFG 1.0). 24GB: fp8 comfortable (one expert resident ~14GB); fp16 needs offload.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"primary": true,
|
||||
"variants": [
|
||||
{ "precision": "fp8_scaled", "filename": "wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors", "size_bytes": 14293923632, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "wan2.2_t2v_high_noise_14B_fp16.safetensors", "size_bytes": 28649479008, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_t2v_high_noise_14B_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise)",
|
||||
"folder": "diffusion_models",
|
||||
"variants": [
|
||||
{ "precision": "fp8_scaled", "filename": "wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors", "size_bytes": 14293923632, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "wan2.2_t2v_low_noise_14B_fp16.safetensors", "size_bytes": 28649479008, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/diffusion_models/wan2.2_t2v_low_noise_14B_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE", "folder": "vae", "filename": "wan_2.1_vae.safetensors", "size_bytes": 253815318, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan_2.1_vae.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - High Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors", "size_bytes": 643825664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - Low Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors", "size_bytes": 643825664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSamplerAdvanced": {
|
||||
"steps": 20,
|
||||
"cfg": 3.5,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [10, 40],
|
||||
"cfg_range": [1.0, 6.0],
|
||||
"sampler_name_options": ["euler", "uni_pc", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"display_name": "Wan 2.2 T2V - 14B (GGUF)",
|
||||
"model_name": "Wan 2.2 T2V GGUF",
|
||||
"version": "14B",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v_gguf",
|
||||
"author": "Alibaba/Wan (GGUF by QuantStack)",
|
||||
"description": "14B text-to-video, GGUF-quantized for lower VRAM. Dual-expert MoE loaded via UnetLoaderGGUF (requires the ComfyUI-GGUF node pack). Pick a quantization to download (Q5_K_M recommended for 24GB).",
|
||||
"license": "Apache 2.0",
|
||||
"tags": ["wan", "video", "t2v", "text-to-video", "MoE", "14B", "gguf"],
|
||||
"url": "https://huggingface.co/QuantStack/Wan2.2-T2V-A14B-GGUF",
|
||||
"release_date": "2025-12-01",
|
||||
"tag_sources": [],
|
||||
"tag_format": "spaces",
|
||||
"prompt_style": "natural",
|
||||
"trigger": "",
|
||||
"negative": "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"notes": "GGUF requires UnetLoaderGGUF (ComfyUI-GGUF). Templates bake Q5_K_M; if you download a different quant, pick it in the loader dropdown. Same recipe as the safetensors T2V (shift 5.0, vanilla 20 steps / fast 4-step LoRA v1.1). umt5 + VAE are the standard safetensors files.",
|
||||
"files": [
|
||||
{
|
||||
"label": "Diffusion Model (High Noise) - GGUF",
|
||||
"folder": "unet",
|
||||
"primary": true,
|
||||
"variants": [
|
||||
{ "precision": "Q5_K_M", "filename": "Wan2.2-T2V-A14B-HighNoise-Q5_K_M.gguf", "size_bytes": 10800000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "HighNoise/Wan2.2-T2V-A14B-HighNoise-Q5_K_M.gguf" }, "sha256": null },
|
||||
{ "precision": "Q8_0", "filename": "Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf", "size_bytes": 16000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "HighNoise/Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf" }, "sha256": null },
|
||||
{ "precision": "Q6_K", "filename": "Wan2.2-T2V-A14B-HighNoise-Q6_K.gguf", "size_bytes": 13000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "HighNoise/Wan2.2-T2V-A14B-HighNoise-Q6_K.gguf" }, "sha256": null },
|
||||
{ "precision": "Q4_K_M", "filename": "Wan2.2-T2V-A14B-HighNoise-Q4_K_M.gguf", "size_bytes": 9000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "HighNoise/Wan2.2-T2V-A14B-HighNoise-Q4_K_M.gguf" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Diffusion Model (Low Noise) - GGUF",
|
||||
"folder": "unet",
|
||||
"variants": [
|
||||
{ "precision": "Q5_K_M", "filename": "Wan2.2-T2V-A14B-LowNoise-Q5_K_M.gguf", "size_bytes": 10800000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "LowNoise/Wan2.2-T2V-A14B-LowNoise-Q5_K_M.gguf" }, "sha256": null },
|
||||
{ "precision": "Q8_0", "filename": "Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf", "size_bytes": 16000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "LowNoise/Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf" }, "sha256": null },
|
||||
{ "precision": "Q6_K", "filename": "Wan2.2-T2V-A14B-LowNoise-Q6_K.gguf", "size_bytes": 13000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "LowNoise/Wan2.2-T2V-A14B-LowNoise-Q6_K.gguf" }, "sha256": null },
|
||||
{ "precision": "Q4_K_M", "filename": "Wan2.2-T2V-A14B-LowNoise-Q4_K_M.gguf", "size_bytes": 9000000000, "source": { "provider": "huggingface", "repo": "QuantStack/Wan2.2-T2V-A14B-GGUF", "path": "LowNoise/Wan2.2-T2V-A14B-LowNoise-Q4_K_M.gguf" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Text Encoder (UMT5-XXL)",
|
||||
"folder": "text_encoders",
|
||||
"variants": [
|
||||
{ "precision": "fp8", "filename": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size_bytes": 6735906897, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" }, "sha256": null },
|
||||
{ "precision": "fp16", "filename": "umt5_xxl_fp16.safetensors", "size_bytes": 11401671879, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/text_encoders/umt5_xxl_fp16.safetensors" }, "sha256": null }
|
||||
]
|
||||
},
|
||||
{ "label": "VAE", "folder": "vae", "filename": "wan_2.1_vae.safetensors", "size_bytes": 253815318, "source": { "provider": "huggingface", "repo": "Comfy-Org/Wan_2.2_ComfyUI_Repackaged", "path": "split_files/vae/wan_2.1_vae.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - High Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors", "size_bytes": 643825664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors" }, "sha256": null },
|
||||
{ "label": "LightX2V 4-step LoRA - Low Noise (Fast, rank64 build)", "folder": "loras", "optional": true, "filename": "wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors", "size_bytes": 643825664, "source": { "provider": "huggingface", "repo": "lightx2v/Wan2.2-Distill-Loras", "path": "wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors" }, "sha256": null }
|
||||
],
|
||||
"nodes": {
|
||||
"KSamplerAdvanced": {
|
||||
"steps": 20,
|
||||
"cfg": 3.5,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"steps_range": [10, 40],
|
||||
"cfg_range": [1.0, 6.0],
|
||||
"sampler_name_options": ["euler", "uni_pc", "dpmpp_2m"],
|
||||
"scheduler_options": ["simple", "normal", "beta"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "ideogram-default",
|
||||
"name": "Default",
|
||||
"category": "Photographic",
|
||||
"text": "// Your description\n{cursor}\n\n// Photographic - Default\nA candid natural-light photograph, soft daylight, true-to-life color and texture, sharp focus with a gently blurred background, relaxed off-center framing.",
|
||||
"text": "// Write your image in plain words. Optional scopes you can add:\n// $background{ a misty forest at dusk } = the scene / background\n// $alice{ red dress }, $bob{ blue suit } = one per subject (keeps them from blending; draw a box in the Region Box panel to place each)\n// Lines starting with // are just notes — they are ignored.\n{cursor}\n\nA candid natural-light photograph, soft daylight, true-to-life color, sharp focus.",
|
||||
"scope": {
|
||||
"type": "architecture",
|
||||
"architecture": "ideogram"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Wan 2.2 Video Prompts",
|
||||
"prompts": [
|
||||
{
|
||||
"id": "wan-i2v",
|
||||
"name": "Video (I2V)",
|
||||
"category": "Video",
|
||||
"text": "// Video Description\n{cursor}\n\nNegative Prompt: 色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"scope": { "type": "architecture", "architecture": "wan22" }
|
||||
},
|
||||
{
|
||||
"id": "wan-t2v",
|
||||
"name": "Video (T2V)",
|
||||
"category": "Video",
|
||||
"text": "// Video Description\n{cursor}\n\nNegative Prompt: 色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",
|
||||
"scope": { "type": "architecture", "architecture": "wan22" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "Regional — Region Boxes",
|
||||
"category": "Generation",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "flux",
|
||||
"family": "flux_dev"
|
||||
},
|
||||
"created_at": 1781740800,
|
||||
"id": "flux-t2i-regionbox",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [1560, 0],
|
||||
"size": [270, 400],
|
||||
"widgets_values": [3257151735, "randomize", 40, 1, "euler", "simple", 1],
|
||||
"properties": { "Node name for S&R": "KSampler" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1900, 0],
|
||||
"size": [210, 46],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "SaveImage",
|
||||
"offset": [1900, 120],
|
||||
"size": [320, 380],
|
||||
"widgets_values": ["flux1/%date:yyyy-MM-dd_hhmmss%"],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"type": "DualCLIPLoader",
|
||||
"offset": [820, 140],
|
||||
"size": [300, 130],
|
||||
"widgets_values": ["t5xxl_fp16.safetensors", "clip_l.safetensors", "flux", "default"],
|
||||
"properties": { "Node name for S&R": "DualCLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [820, 0],
|
||||
"size": [300, 82],
|
||||
"widgets_values": ["flux1-dev.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "EmptySD3LatentImage",
|
||||
"offset": [820, 320],
|
||||
"size": [270, 106],
|
||||
"widgets_values": [832, 1216, 1],
|
||||
"properties": { "Node name for S&R": "EmptySD3LatentImage" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [820, 470],
|
||||
"size": [246, 58],
|
||||
"widgets_values": ["ae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_RegionalConditioning",
|
||||
"offset": [1200, 0],
|
||||
"size": [300, 150],
|
||||
"widgets_values": [22],
|
||||
"properties": { "Node name for S&R": "PromptChain_RegionalConditioning" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_RegionBox",
|
||||
"offset": [1190, 200],
|
||||
"size": [340, 400],
|
||||
"widgets_values": ["", 832, 1216, 0],
|
||||
"properties": { "Node name for S&R": "PromptChain_RegionBox" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 0, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 7, "to_slot": 0 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 0, "to_slot": 1 },
|
||||
{ "from_node_idx": 7, "from_slot": 1, "to_node_idx": 0, "to_slot": 2 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 7, "to_slot": 2 },
|
||||
{ "from_node_idx": 8, "from_slot": 1, "to_node_idx": 7, "to_slot": 3 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 0, "to_slot": 3 },
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 1, "to_slot": 1 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 3, "to_node_idx": 7, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
"name": "Regional — Region Boxes",
|
||||
"category": "Generation",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "architecture",
|
||||
"architecture": "ideogram"
|
||||
},
|
||||
"created_at": 1781740800,
|
||||
"id": "ideogram-t2i-regionbox",
|
||||
"prompt_id": "ideogram-default",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [900, 0],
|
||||
"size": [225, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "ConditioningZeroOut",
|
||||
"offset": [1180, 0],
|
||||
"size": [240, 48],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "ConditioningZeroOut" }
|
||||
},
|
||||
{
|
||||
"type": "CFGOverride",
|
||||
"offset": [1180, 120],
|
||||
"size": [280, 106],
|
||||
"widgets_values": [3.0, 0.7, 1.0],
|
||||
"properties": { "Node name for S&R": "CFGOverride" }
|
||||
},
|
||||
{
|
||||
"type": "DualModelGuider",
|
||||
"offset": [1500, 120],
|
||||
"size": [266, 150],
|
||||
"widgets_values": [7.0],
|
||||
"properties": { "Node name for S&R": "DualModelGuider" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_IdeogramSampler",
|
||||
"offset": [1840, 360],
|
||||
"size": [310, 220],
|
||||
"widgets_values": [4, 0.08],
|
||||
"properties": { "Node name for S&R": "PromptChain_IdeogramSampler" }
|
||||
},
|
||||
{
|
||||
"type": "SaveImage",
|
||||
"offset": [2440, 0],
|
||||
"size": [400, 460],
|
||||
"widgets_values": ["Ideogram4/%date:yyyy-MM-dd%"],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [820, 200],
|
||||
"size": [320, 82],
|
||||
"widgets_values": ["ideogram4_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [820, 320],
|
||||
"size": [320, 82],
|
||||
"widgets_values": ["ideogram4_unconditional_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [560, 0],
|
||||
"size": [320, 110],
|
||||
"widgets_values": ["qwen3vl_8b_fp8_scaled.safetensors", "ideogram4", "default"],
|
||||
"properties": { "Node name for S&R": "CLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [1500, 300],
|
||||
"size": [246, 58],
|
||||
"widgets_values": ["flux2-vae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "RandomNoise",
|
||||
"offset": [1500, 400],
|
||||
"size": [260, 82],
|
||||
"widgets_values": [0, "randomize"],
|
||||
"properties": { "Node name for S&R": "RandomNoise" }
|
||||
},
|
||||
{
|
||||
"type": "KSamplerSelect",
|
||||
"offset": [1500, 520],
|
||||
"size": [240, 58],
|
||||
"widgets_values": ["euler"],
|
||||
"properties": { "Node name for S&R": "KSamplerSelect" }
|
||||
},
|
||||
{
|
||||
"type": "Ideogram4Scheduler",
|
||||
"offset": [1500, 620],
|
||||
"size": [300, 154],
|
||||
"widgets_values": [20, 1024, 1024, 0.5, 1.75],
|
||||
"properties": { "Node name for S&R": "Ideogram4Scheduler" }
|
||||
},
|
||||
{
|
||||
"type": "EmptyFlux2LatentImage",
|
||||
"offset": [1500, 800],
|
||||
"size": [290, 106],
|
||||
"widgets_values": [1024, 1024, 1],
|
||||
"properties": { "Node name for S&R": "EmptyFlux2LatentImage" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_IdeogramCaption",
|
||||
"offset": [600, 180],
|
||||
"size": [250, 82],
|
||||
"widgets_values": ["caption"],
|
||||
"properties": { "Node name for S&R": "PromptChain_IdeogramCaption" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_RegionBox",
|
||||
"offset": [560, 300],
|
||||
"size": [340, 400],
|
||||
"widgets_values": ["", 1024, 1024, 0],
|
||||
"properties": { "Node name for S&R": "PromptChain_RegionBox" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 0, "to_slot": 0 },
|
||||
{ "from_node_idx": 14, "from_slot": 0, "to_node_idx": 0, "to_slot": 1 },
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 3, "to_slot": 1 },
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 3, "to_slot": 3 },
|
||||
{ "from_node_idx": 6, "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": 7, "from_slot": 0, "to_node_idx": 3, "to_slot": 2 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 1 },
|
||||
{ "from_node_idx": 10, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
|
||||
{ "from_node_idx": 11, "from_slot": 0, "to_node_idx": 4, "to_slot": 2 },
|
||||
{ "from_node_idx": 12, "from_slot": 0, "to_node_idx": 4, "to_slot": 3 },
|
||||
{ "from_node_idx": 13, "from_slot": 0, "to_node_idx": 4, "to_slot": 4 },
|
||||
{ "from_node_idx": 9, "from_slot": 0, "to_node_idx": 4, "to_slot": 5 },
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 5, "to_slot": 0 },
|
||||
{ "from_node_idx": 15, "from_slot": 1, "to_node_idx": 14, "to_slot": 2 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 14, "to_slot": 0 },
|
||||
{ "from_slot": 3, "to_node_idx": 14, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
@@ -112,7 +112,7 @@
|
||||
"type": "PromptChain_IdeogramCaption",
|
||||
"offset": [600, 180],
|
||||
"size": [250, 82],
|
||||
"widgets_values": ["envelope"],
|
||||
"widgets_values": ["caption"],
|
||||
"properties": { "Node name for S&R": "PromptChain_IdeogramCaption" }
|
||||
}
|
||||
],
|
||||
@@ -134,6 +134,7 @@
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 5, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 14, "to_slot": 0 }
|
||||
{ "from_slot": 1, "to_node_idx": 14, "to_slot": 0 },
|
||||
{ "from_slot": 3, "to_node_idx": 14, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "Regional — Region Boxes",
|
||||
"category": "Generation",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "architecture",
|
||||
"architecture": "sdxl"
|
||||
},
|
||||
"created_at": 1781740800,
|
||||
"id": "sdxl-t2i-regionbox",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [1560, 0],
|
||||
"size": [300, 474],
|
||||
"widgets_values": [674787291728667, "randomize", 30, 5, "euler", "normal", 1],
|
||||
"properties": { "Node name for S&R": "KSampler" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1900, 0],
|
||||
"size": [210, 46],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "SaveImage",
|
||||
"offset": [1900, 120],
|
||||
"size": [320, 380],
|
||||
"widgets_values": ["{model_name}/%date:yyyy-MM-dd_hhmmss%"],
|
||||
"properties": {}
|
||||
},
|
||||
{
|
||||
"type": "CLIPSetLastLayer",
|
||||
"offset": [820, 220],
|
||||
"size": [260, 60],
|
||||
"widgets_values": [-2],
|
||||
"properties": { "Node name for S&R": "CLIPSetLastLayer" }
|
||||
},
|
||||
{
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"offset": [820, 0],
|
||||
"size": [360, 100],
|
||||
"widgets_values": ["PVCStyleModelMovable_epsIll11.safetensors"],
|
||||
"properties": { "Node name for S&R": "CheckpointLoaderSimple" }
|
||||
},
|
||||
{
|
||||
"type": "EmptyLatentImage",
|
||||
"offset": [820, 320],
|
||||
"size": [300, 106],
|
||||
"widgets_values": [832, 1216, 1],
|
||||
"properties": { "Node name for S&R": "EmptyLatentImage" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_AttentionCouple",
|
||||
"offset": [1200, 0],
|
||||
"size": [300, 170],
|
||||
"widgets_values": [0.5, 22],
|
||||
"properties": { "Node name for S&R": "PromptChain_AttentionCouple" }
|
||||
},
|
||||
{
|
||||
"type": "PromptChain_RegionBox",
|
||||
"offset": [1190, 220],
|
||||
"size": [340, 400],
|
||||
"widgets_values": ["", 832, 1216, 0],
|
||||
"properties": { "Node name for S&R": "PromptChain_RegionBox" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 6, "to_slot": 0 },
|
||||
{ "from_node_idx": 4, "from_slot": 1, "to_node_idx": 3, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 6, "to_slot": 1 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 0, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 1, "to_node_idx": 0, "to_slot": 1 },
|
||||
{ "from_node_idx": 6, "from_slot": 2, "to_node_idx": 0, "to_slot": 2 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 6, "to_slot": 3 },
|
||||
{ "from_node_idx": 7, "from_slot": 1, "to_node_idx": 6, "to_slot": 4 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 0, "to_slot": 3 },
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 4, "from_slot": 2, "to_node_idx": 1, "to_slot": 1 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 3, "to_node_idx": 6, "to_slot": 2 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"name": "Image-to-Video (5B, GGUF)",
|
||||
"category": "Video",
|
||||
"order": 2,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-5b-i2v-gguf",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-TI2V-5B-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
140
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Wan22ImageToVideoLatent",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"widgets_values": [
|
||||
1280,
|
||||
704,
|
||||
121,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Wan22ImageToVideoLatent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
262
|
||||
],
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
5,
|
||||
"uni_pc",
|
||||
"simple",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
24
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
+325
-109
@@ -1,109 +1,325 @@
|
||||
{
|
||||
"name": "Image-to-Video",
|
||||
"category": "Video",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b"
|
||||
},
|
||||
"created_at": 1775178000,
|
||||
"id": "wan22-5b-i2v",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 0],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_ti2v_5B_fp16.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 120],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [60, 250],
|
||||
"size": [340, 117],
|
||||
"widgets_values": ["umt5_xxl_fp8_e4m3fn_scaled.safetensors", "wan", "default"],
|
||||
"properties": { "Node name for S&R": "CLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [60, 420],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [60, 580],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [500, 0],
|
||||
"size": [340, 460],
|
||||
"widgets_values": ["", "image"],
|
||||
"properties": { "Node name for S&R": "LoadImage" }
|
||||
},
|
||||
{
|
||||
"type": "Wan22ImageToVideoLatent",
|
||||
"offset": [500, 500],
|
||||
"size": [300, 200],
|
||||
"widgets_values": [1280, 704, 49],
|
||||
"properties": { "Node name for S&R": "Wan22ImageToVideoLatent" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [900, 0],
|
||||
"size": [290, 83],
|
||||
"widgets_values": ["wan2.2_vae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [900, 130],
|
||||
"size": [300, 500],
|
||||
"widgets_values": [0, "randomize", 30, 4.0, "euler", "simple", 1.0],
|
||||
"properties": { "Node name for S&R": "KSampler" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1260, 0],
|
||||
"size": [240, 72],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [1260, 120],
|
||||
"size": [330, 400],
|
||||
"widgets_values": [16, "image/gif", 0],
|
||||
"properties": { "Node name for S&R": "CreateVideo" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 8, "to_slot": 0 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 3, "to_slot": 0 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 8, "to_slot": 1 },
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 8, "to_slot": 2 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 6, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 8, "to_slot": 3 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 6, "to_slot": 1 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 9, "to_slot": 1 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 9, "to_slot": 0 },
|
||||
{ "from_node_idx": 9, "from_slot": 0, "to_node_idx": 10, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 3, "to_slot": 1 },
|
||||
{ "from_slot": 2, "to_node_idx": 4, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Image-to-Video (5B)",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-5b-i2v",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
140
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Wan22ImageToVideoLatent",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"widgets_values": [
|
||||
1280,
|
||||
704,
|
||||
121,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Wan22ImageToVideoLatent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
262
|
||||
],
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
5,
|
||||
"uni_pc",
|
||||
"simple",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
24
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"name": "Text-to-Video (5B, GGUF)",
|
||||
"category": "Video",
|
||||
"order": 3,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-5b-t2v-gguf",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-TI2V-5B-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
140
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Wan22ImageToVideoLatent",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"widgets_values": [
|
||||
1280,
|
||||
704,
|
||||
121,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Wan22ImageToVideoLatent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
262
|
||||
],
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
5,
|
||||
"uni_pc",
|
||||
"simple",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
24
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
+301
-100
@@ -1,100 +1,301 @@
|
||||
{
|
||||
"name": "Text-to-Video",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b"
|
||||
},
|
||||
"created_at": 1775178000,
|
||||
"id": "wan22-5b-t2v",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 0],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_ti2v_5B_fp16.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 120],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [60, 250],
|
||||
"size": [340, 117],
|
||||
"widgets_values": ["umt5_xxl_fp8_e4m3fn_scaled.safetensors", "wan", "default"],
|
||||
"properties": { "Node name for S&R": "CLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [60, 420],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [60, 580],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [500, 0],
|
||||
"size": [290, 170],
|
||||
"widgets_values": [1280, 704, 49, 1],
|
||||
"properties": { "Node name for S&R": "EmptyHunyuanLatentVideo" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [500, 210],
|
||||
"size": [290, 83],
|
||||
"widgets_values": ["wan2.2_vae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [860, 0],
|
||||
"size": [300, 500],
|
||||
"widgets_values": [0, "randomize", 30, 4.0, "euler", "simple", 1.0],
|
||||
"properties": { "Node name for S&R": "KSampler" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1220, 0],
|
||||
"size": [240, 72],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [1220, 120],
|
||||
"size": [330, 400],
|
||||
"widgets_values": [16, "image/gif", 0],
|
||||
"properties": { "Node name for S&R": "CreateVideo" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 7, "to_slot": 0 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 3, "to_slot": 0 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 7, "to_slot": 1 },
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 7, "to_slot": 2 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 7, "to_slot": 3 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 8, "to_slot": 1 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 8, "to_slot": 0 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 9, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 3, "to_slot": 1 },
|
||||
{ "from_slot": 2, "to_node_idx": 4, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Text-to-Video (5B)",
|
||||
"category": "Video",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_5b"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-5b-t2v",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_ti2v_5B_fp16.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
140
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Wan22ImageToVideoLatent",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
150
|
||||
],
|
||||
"widgets_values": [
|
||||
1280,
|
||||
704,
|
||||
121,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Wan22ImageToVideoLatent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSampler",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
262
|
||||
],
|
||||
"widgets_values": [
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
5,
|
||||
"uni_pc",
|
||||
"simple",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
24
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 3,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
{
|
||||
"name": "First-Last-Frame (FLF2V, Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 4,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-flf2v-fast",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "Start Frame"
|
||||
},
|
||||
{
|
||||
"type": "WanFirstLastFrameToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanFirstLastFrameToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
1100
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "End Frame"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 5
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 16,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 17,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 6
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
{
|
||||
"name": "First-Last-Frame (FLF2V, GGUF, Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 5,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-flf2v-gguf-fast",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "Start Frame"
|
||||
},
|
||||
{
|
||||
"type": "WanFirstLastFrameToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanFirstLastFrameToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
1100
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "End Frame"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 5
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 16,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 17,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 6
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
{
|
||||
"name": "First-Last-Frame (FLF2V, GGUF)",
|
||||
"category": "Video",
|
||||
"order": 3,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-flf2v-gguf",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "Start Frame"
|
||||
},
|
||||
{
|
||||
"type": "WanFirstLastFrameToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanFirstLastFrameToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
4,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
4,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
1100
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "End Frame"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 5
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 6
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
{
|
||||
"name": "First-Last-Frame (FLF2V)",
|
||||
"category": "Video",
|
||||
"order": 2,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-flf2v",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
8
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "Start Frame"
|
||||
},
|
||||
{
|
||||
"type": "WanFirstLastFrameToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanFirstLastFrameToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
4,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
4,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
1100
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"title": "End Frame"
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 5
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 6
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
{
|
||||
"name": "Image-to-Video (Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-i2v-fast",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WanImageToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanImageToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 4
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 16,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"name": "Image-to-Video (GGUF, Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 3,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-i2v-gguf-fast",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_high_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_A14b_low_noise_lora_rank64_lightx2v_4step_1022.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WanImageToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanImageToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 4
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 15,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 16,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
{
|
||||
"name": "Image-to-Video (GGUF)",
|
||||
"category": "Video",
|
||||
"order": 2,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-i2v-gguf",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-I2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WanImageToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanImageToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 4
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
+431
-153
@@ -1,153 +1,431 @@
|
||||
{
|
||||
"name": "Image-to-Video",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v"
|
||||
},
|
||||
"created_at": 1775178000,
|
||||
"id": "wan22-i2v",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 0],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [60, 120],
|
||||
"size": [355, 112],
|
||||
"widgets_values": ["wan2.2_i2v_lightx2v_4steps_lora_v1_high_noise.safetensors", 1.0],
|
||||
"properties": { "Node name for S&R": "LoraLoaderModelOnly" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 280],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 410],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [60, 530],
|
||||
"size": [355, 112],
|
||||
"widgets_values": ["wan2.2_i2v_lightx2v_4steps_lora_v1_low_noise.safetensors", 1.0],
|
||||
"properties": { "Node name for S&R": "LoraLoaderModelOnly" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 690],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [500, 0],
|
||||
"size": [340, 117],
|
||||
"widgets_values": ["umt5_xxl_fp8_e4m3fn_scaled.safetensors", "wan", "default"],
|
||||
"properties": { "Node name for S&R": "CLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [500, 160],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [500, 320],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [500, 480],
|
||||
"size": [290, 83],
|
||||
"widgets_values": ["wan_2.1_vae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [500, 610],
|
||||
"size": [340, 460],
|
||||
"widgets_values": ["", "image"],
|
||||
"properties": { "Node name for S&R": "LoadImage" }
|
||||
},
|
||||
{
|
||||
"type": "WanImageToVideo",
|
||||
"offset": [900, 0],
|
||||
"size": [300, 220],
|
||||
"widgets_values": [640, 640, 81, 1],
|
||||
"properties": { "Node name for S&R": "WanImageToVideo" }
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [1260, 0],
|
||||
"size": [300, 504],
|
||||
"widgets_values": ["enable", 0, "randomize", 4, 1.0, "euler", "simple", 0, 2, "enable"],
|
||||
"properties": { "Node name for S&R": "KSamplerAdvanced" }
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [1260, 550],
|
||||
"size": [300, 504],
|
||||
"widgets_values": ["disable", 0, "fixed", 4, 1.0, "euler", "simple", 2, 4, "disable"],
|
||||
"properties": { "Node name for S&R": "KSamplerAdvanced" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1620, 0],
|
||||
"size": [240, 72],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [1620, 120],
|
||||
"size": [330, 400],
|
||||
"widgets_values": [16],
|
||||
"properties": { "Node name for S&R": "CreateVideo" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 5, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 7, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 8, "to_slot": 0 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 11, "to_slot": 0 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 11, "to_slot": 1 },
|
||||
{ "from_node_idx": 9, "from_slot": 0, "to_node_idx": 11, "to_slot": 2 },
|
||||
{ "from_node_idx": 9, "from_slot": 0, "to_node_idx": 14, "to_slot": 1 },
|
||||
{ "from_node_idx": 10, "from_slot": 0, "to_node_idx": 11, "to_slot": 4 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 12, "to_slot": 0 },
|
||||
{ "from_node_idx": 11, "from_slot": 0, "to_node_idx": 12, "to_slot": 1 },
|
||||
{ "from_node_idx": 11, "from_slot": 1, "to_node_idx": 12, "to_slot": 2 },
|
||||
{ "from_node_idx": 11, "from_slot": 2, "to_node_idx": 12, "to_slot": 3 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 13, "to_slot": 0 },
|
||||
{ "from_node_idx": 11, "from_slot": 0, "to_node_idx": 13, "to_slot": 1 },
|
||||
{ "from_node_idx": 11, "from_slot": 1, "to_node_idx": 13, "to_slot": 2 },
|
||||
{ "from_node_idx": 12, "from_slot": 0, "to_node_idx": 13, "to_slot": 3 },
|
||||
{ "from_node_idx": 13, "from_slot": 0, "to_node_idx": 14, "to_slot": 0 },
|
||||
{ "from_node_idx": 14, "from_slot": 0, "to_node_idx": 15, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 7, "to_slot": 1 },
|
||||
{ "from_slot": 2, "to_node_idx": 8, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Image-to-Video",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_i2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-i2v",
|
||||
"prompt_id": "wan-i2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoadImage",
|
||||
"offset": [
|
||||
420,
|
||||
610
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
460
|
||||
],
|
||||
"widgets_values": [
|
||||
"",
|
||||
"image"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "WanImageToVideo",
|
||||
"offset": [
|
||||
780,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
220
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "WanImageToVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 4
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
{
|
||||
"name": "Text-to-Video (Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 1,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-t2v-fast",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
144
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyHunyuanLatentVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
660
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"name": "Text-to-Video (GGUF, Fast 4-step)",
|
||||
"category": "Video",
|
||||
"order": 3,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-t2v-gguf-fast",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-T2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_A14b_high_noise_lora_rank64_lightx2v_4step_1217.safetensors",
|
||||
0.7
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-T2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [
|
||||
60,
|
||||
540
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
112
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_A14b_low_noise_lora_rank64_lightx2v_4step_1217.safetensors",
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoraLoaderModelOnly"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
144
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyHunyuanLatentVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
660
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
2,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
4,
|
||||
1,
|
||||
"euler",
|
||||
"simple",
|
||||
2,
|
||||
4,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 2,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 4,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 2,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 13,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 14,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 14,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 15,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 7,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 8,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
{
|
||||
"name": "Text-to-Video (GGUF)",
|
||||
"category": "Video",
|
||||
"order": 2,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v_gguf"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-t2v-gguf",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-T2V-A14B-HighNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UnetLoaderGGUF",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"Wan2.2-T2V-A14B-LowNoise-Q5_K_M.gguf"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UnetLoaderGGUF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
144
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyHunyuanLatentVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
660
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
+389
-142
@@ -1,142 +1,389 @@
|
||||
{
|
||||
"name": "Text-to-Video",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v"
|
||||
},
|
||||
"created_at": 1775178000,
|
||||
"id": "wan22-t2v",
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 0],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 120],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [60, 240],
|
||||
"size": [355, 112],
|
||||
"widgets_values": ["wan2.2_t2v_lightx2v_4steps_lora_v1.1_high_noise.safetensors", 1.0],
|
||||
"properties": { "Node name for S&R": "LoraLoaderModelOnly" }
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [60, 400],
|
||||
"size": [355, 83],
|
||||
"widgets_values": ["wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors", "default"],
|
||||
"properties": { "Node name for S&R": "UNETLoader" }
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [60, 520],
|
||||
"size": [315, 80],
|
||||
"widgets_values": [5.0],
|
||||
"properties": { "Node name for S&R": "ModelSamplingSD3" }
|
||||
},
|
||||
{
|
||||
"type": "LoraLoaderModelOnly",
|
||||
"offset": [60, 640],
|
||||
"size": [355, 112],
|
||||
"widgets_values": ["wan2.2_t2v_lightx2v_4steps_lora_v1.1_low_noise.safetensors", 1.0],
|
||||
"properties": { "Node name for S&R": "LoraLoaderModelOnly" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [500, 0],
|
||||
"size": [340, 117],
|
||||
"widgets_values": ["umt5_xxl_fp8_e4m3fn_scaled.safetensors", "wan", "default"],
|
||||
"properties": { "Node name for S&R": "CLIPLoader" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [500, 160],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [500, 320],
|
||||
"size": [340, 116],
|
||||
"widgets_values": [""],
|
||||
"properties": { "Node name for S&R": "CLIPTextEncode" }
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [500, 480],
|
||||
"size": [290, 144],
|
||||
"widgets_values": [640, 640, 81, 1],
|
||||
"properties": { "Node name for S&R": "EmptyHunyuanLatentVideo" }
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [900, 0],
|
||||
"size": [290, 83],
|
||||
"widgets_values": ["wan_2.1_vae.safetensors"],
|
||||
"properties": { "Node name for S&R": "VAELoader" }
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [1260, 0],
|
||||
"size": [300, 504],
|
||||
"widgets_values": ["enable", 0, "randomize", 4, 1.0, "euler", "simple", 0, 2, "enable"],
|
||||
"properties": { "Node name for S&R": "KSamplerAdvanced" }
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [1260, 550],
|
||||
"size": [300, 504],
|
||||
"widgets_values": ["disable", 0, "randomize", 4, 1.0, "euler", "simple", 2, 4, "disable"],
|
||||
"properties": { "Node name for S&R": "KSamplerAdvanced" }
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [1620, 0],
|
||||
"size": [240, 72],
|
||||
"widgets_values": [],
|
||||
"properties": { "Node name for S&R": "VAEDecode" }
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [1620, 120],
|
||||
"size": [330, 400],
|
||||
"widgets_values": [16, "image/gif", 0],
|
||||
"properties": { "Node name for S&R": "CreateVideo" }
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{ "from_node_idx": 0, "from_slot": 0, "to_node_idx": 1, "to_slot": 0 },
|
||||
{ "from_node_idx": 1, "from_slot": 0, "to_node_idx": 2, "to_slot": 0 },
|
||||
{ "from_node_idx": 2, "from_slot": 0, "to_node_idx": 11, "to_slot": 0 },
|
||||
{ "from_node_idx": 3, "from_slot": 0, "to_node_idx": 4, "to_slot": 0 },
|
||||
{ "from_node_idx": 4, "from_slot": 0, "to_node_idx": 5, "to_slot": 0 },
|
||||
{ "from_node_idx": 5, "from_slot": 0, "to_node_idx": 12, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 7, "to_slot": 0 },
|
||||
{ "from_node_idx": 6, "from_slot": 0, "to_node_idx": 8, "to_slot": 0 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 11, "to_slot": 1 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 11, "to_slot": 2 },
|
||||
{ "from_node_idx": 8, "from_slot": 0, "to_node_idx": 12, "to_slot": 2 },
|
||||
{ "from_node_idx": 9, "from_slot": 0, "to_node_idx": 11, "to_slot": 3 },
|
||||
{ "from_node_idx": 7, "from_slot": 0, "to_node_idx": 12, "to_slot": 1 },
|
||||
{ "from_node_idx": 11, "from_slot": 0, "to_node_idx": 12, "to_slot": 3 },
|
||||
{ "from_node_idx": 10, "from_slot": 0, "to_node_idx": 13, "to_slot": 1 },
|
||||
{ "from_node_idx": 12, "from_slot": 0, "to_node_idx": 13, "to_slot": 0 },
|
||||
{ "from_node_idx": 13, "from_slot": 0, "to_node_idx": 14, "to_slot": 0 }
|
||||
],
|
||||
"anchorConnections": [
|
||||
{ "from_slot": 1, "to_node_idx": 7, "to_slot": 1 },
|
||||
{ "from_slot": 2, "to_node_idx": 8, "to_slot": 1 }
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "Text-to-Video",
|
||||
"category": "Video",
|
||||
"order": 0,
|
||||
"scope": {
|
||||
"type": "family",
|
||||
"architecture": "wan22",
|
||||
"family": "wan22_t2v"
|
||||
},
|
||||
"created_at": 1782000000,
|
||||
"id": "wan22-t2v",
|
||||
"prompt_id": "wan-t2v",
|
||||
"lockModels": true,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_high_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
280
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "UNETLoader",
|
||||
"offset": [
|
||||
60,
|
||||
420
|
||||
],
|
||||
"size": [
|
||||
355,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan2.2_t2v_low_noise_14B_fp8_scaled.safetensors",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "UNETLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ModelSamplingSD3",
|
||||
"offset": [
|
||||
60,
|
||||
700
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
80
|
||||
],
|
||||
"widgets_values": [
|
||||
5
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ModelSamplingSD3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPLoader",
|
||||
"offset": [
|
||||
420,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
117
|
||||
],
|
||||
"widgets_values": [
|
||||
"umt5_xxl_fp8_e4m3fn_scaled.safetensors",
|
||||
"wan",
|
||||
"default"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPLoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
160
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CLIPTextEncode",
|
||||
"offset": [
|
||||
420,
|
||||
320
|
||||
],
|
||||
"size": [
|
||||
340,
|
||||
116
|
||||
],
|
||||
"widgets_values": [
|
||||
""
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "EmptyHunyuanLatentVideo",
|
||||
"offset": [
|
||||
420,
|
||||
480
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
144
|
||||
],
|
||||
"widgets_values": [
|
||||
640,
|
||||
640,
|
||||
81,
|
||||
1
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyHunyuanLatentVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAELoader",
|
||||
"offset": [
|
||||
420,
|
||||
660
|
||||
],
|
||||
"size": [
|
||||
290,
|
||||
83
|
||||
],
|
||||
"widgets_values": [
|
||||
"wan_2.1_vae.safetensors"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAELoader"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"enable",
|
||||
0,
|
||||
"randomize",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
0,
|
||||
10,
|
||||
"enable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "KSamplerAdvanced",
|
||||
"offset": [
|
||||
1140,
|
||||
560
|
||||
],
|
||||
"size": [
|
||||
300,
|
||||
504
|
||||
],
|
||||
"widgets_values": [
|
||||
"disable",
|
||||
0,
|
||||
"fixed",
|
||||
20,
|
||||
3.5,
|
||||
"euler",
|
||||
"simple",
|
||||
10,
|
||||
20,
|
||||
"disable"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSamplerAdvanced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "VAEDecode",
|
||||
"offset": [
|
||||
1500,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
240,
|
||||
72
|
||||
],
|
||||
"widgets_values": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CreateVideo",
|
||||
"offset": [
|
||||
1500,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
120
|
||||
],
|
||||
"widgets_values": [
|
||||
16
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CreateVideo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "SaveVideo",
|
||||
"offset": [
|
||||
1860,
|
||||
120
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
200
|
||||
],
|
||||
"widgets_values": [
|
||||
"video/PromptChain",
|
||||
"auto",
|
||||
"auto"
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveVideo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"from_node_idx": 0,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 1,
|
||||
"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": 5,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 4,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 1,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 7,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 9,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 3,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 5,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 6,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 2
|
||||
},
|
||||
{
|
||||
"from_node_idx": 9,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 10,
|
||||
"to_slot": 3
|
||||
},
|
||||
{
|
||||
"from_node_idx": 10,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 8,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 11,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_node_idx": 11,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 12,
|
||||
"to_slot": 0
|
||||
},
|
||||
{
|
||||
"from_node_idx": 12,
|
||||
"from_slot": 0,
|
||||
"to_node_idx": 13,
|
||||
"to_slot": 0
|
||||
}
|
||||
],
|
||||
"anchorConnections": [
|
||||
{
|
||||
"from_slot": 1,
|
||||
"to_node_idx": 5,
|
||||
"to_slot": 1
|
||||
},
|
||||
{
|
||||
"from_slot": 2,
|
||||
"to_node_idx": 6,
|
||||
"to_slot": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+27
-1
@@ -73,9 +73,35 @@ async function handleDrop(e) {
|
||||
const canvasX = (e.clientX - rect.left) / canvas.ds.scale - canvas.ds.offset[0];
|
||||
const canvasY = (e.clientY - rect.top) / canvas.ds.scale - canvas.ds.offset[1];
|
||||
|
||||
const scope = assetData.scope || "output";
|
||||
|
||||
// Smart switch: dropping an output image onto a STRICTLY EMPTY graph restores
|
||||
// its embedded ComfyUI workflow (the litegraph "workflow" PNG chunk) instead of
|
||||
// adding a Load Image node — mirroring ComfyUI's native PNG file-drop. Gated to
|
||||
// zero nodes because loadGraphData clean-wipes the canvas (same as the native
|
||||
// drop), so on a populated graph that would silently destroy the user's work.
|
||||
// The endpoint already exists (inpaint/upscale use it) and NaN-defuses the JSON.
|
||||
if (!app.graph?._nodes?.length && /\.(png|webp)$/i.test(assetData.path)) {
|
||||
try {
|
||||
const wfResp = await api.fetchApi(
|
||||
`/promptchain/image-workflow?scope=${scope}&path=${encodeURIComponent(assetData.path)}`
|
||||
);
|
||||
if (wfResp.ok) {
|
||||
const { workflow } = await wfResp.json();
|
||||
if (workflow) {
|
||||
await app.loadGraphData(workflow, true, true, assetData.name || assetData.path.split("/").pop());
|
||||
app.extensionManager?.toast?.add?.({ severity: "success", summary: "Restored workflow from image", life: 2500 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// any failure (no workflow chunk, fetch error) falls through to Load Image
|
||||
log("workflow restore failed; falling back to Load Image", err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// fetch image via browse preview
|
||||
const scope = assetData.scope || "output";
|
||||
const fetchResponse = await api.fetchApi(
|
||||
`/promptchain/browse/preview?scope=${scope}&path=${encodeURIComponent(assetData.path)}`
|
||||
);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// "Extend video" background runner. Builds the I2V continuation graph off-screen
|
||||
// (source video's last frame -> next clip), queues it, and records the result as
|
||||
// a lineage CHILD of the source video (parent_filename) so it joins the family.
|
||||
// The user's open workflow is NEVER touched. Mirrors repose-background.js.
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { recordGeneration, externallyTrackedPrompts, armExternalQueue, disarmExternalQueue } from "./history.js";
|
||||
import { buildExtendGraph } from "./extend-from-image.js";
|
||||
|
||||
function toast(severity, detail, life = 8000) {
|
||||
app.extensionManager?.toast?.add({ severity, summary: "Extend", detail, life });
|
||||
}
|
||||
|
||||
function createTracker() {
|
||||
const listeners = new Set();
|
||||
const tracker = {
|
||||
state: { phase: "building" },
|
||||
subscribe(fn) { listeners.add(fn); fn(tracker.state); return () => listeners.delete(fn); },
|
||||
emit(state) { tracker.state = state; for (const fn of listeners) { try { fn(state); } catch {} } },
|
||||
};
|
||||
return tracker;
|
||||
}
|
||||
|
||||
function viewUrl(img) {
|
||||
if (!img?.filename) return "";
|
||||
const q = new URLSearchParams({ filename: img.filename, subfolder: img.subfolder || "", type: img.type || "output" });
|
||||
return api.apiURL ? api.apiURL(`/view?${q}`) : `/view?${q}`;
|
||||
}
|
||||
|
||||
// opts: { templateId, lastFrameFilename, promptDoc, length, parentFilename }
|
||||
export function runExtendInBackground(opts) {
|
||||
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" });
|
||||
toast("info", "Extend 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("[Extend] cancel request failed", e); }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
tracker.emit({ phase: "building" });
|
||||
// Pass the WHOLE opts through — an explicit allowlist here silently dropped
|
||||
// every Advanced control (recipe sampler knobs, dims, outFps, seed) the modal
|
||||
// sends. buildExtendGraph reads what it needs and ignores the rest.
|
||||
const built = await buildExtendGraph({ ...opts });
|
||||
if (!built) { tracker.emit({ phase: "error", message: "couldn't build the extend graph" }); return; }
|
||||
const { graph, workflowId } = built;
|
||||
if (run.cancelled) return;
|
||||
|
||||
tracker.emit({ phase: "queueing" });
|
||||
const serialized = await app.graphToPrompt(graph);
|
||||
armExternalQueue();
|
||||
let promptId = null;
|
||||
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: serialized.output, workflow: serialized.workflow }, { previewMethod });
|
||||
promptId = res?.prompt_id || null;
|
||||
} catch (e) {
|
||||
disarmExternalQueue();
|
||||
const message = e?.response?.error?.message || e?.message || "queue rejected the prompt";
|
||||
tracker.emit({ phase: "error", message });
|
||||
toast("error", `Extend failed to queue: ${message}`);
|
||||
return;
|
||||
}
|
||||
if (promptId) externallyTrackedPrompts.add(promptId);
|
||||
disarmExternalQueue();
|
||||
run.promptId = promptId;
|
||||
if (!promptId) { tracker.emit({ phase: "error", message: "queue returned no prompt id" }); return; }
|
||||
if (run.cancelled) {
|
||||
try {
|
||||
await api.fetchApi("/queue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: [promptId] }) });
|
||||
} catch {}
|
||||
externallyTrackedPrompts.delete(promptId);
|
||||
return;
|
||||
}
|
||||
|
||||
let lastImage = null, lastPreview = null;
|
||||
const onStart = ({ detail }) => { if (detail?.prompt_id === promptId) run.executionStarted = true; };
|
||||
const onProgress = ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId || run.cancelled) return;
|
||||
run.executionStarted = true;
|
||||
tracker.emit({ ...tracker.state, phase: "running", value: detail?.value || 0, max: detail?.max || 0 });
|
||||
};
|
||||
const onPreview = ({ detail }) => {
|
||||
if (run.cancelled || !run.executionStarted || !(detail instanceof Blob)) return;
|
||||
if (lastPreview) URL.revokeObjectURL(lastPreview);
|
||||
lastPreview = URL.createObjectURL(detail);
|
||||
tracker.emit({ ...tracker.state, phase: "running", previewUrl: lastPreview });
|
||||
};
|
||||
const onExecuted = ({ detail }) => {
|
||||
// SaveVideo emits its output under `images` (PreviewVideo {images,animated}),
|
||||
// same shape as an image save — so the final clip is captured here.
|
||||
if (detail?.prompt_id !== promptId || !detail?.output?.images?.length) return;
|
||||
lastImage = detail.output.images[detail.output.images.length - 1];
|
||||
};
|
||||
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);
|
||||
};
|
||||
run.cleanup = cleanup;
|
||||
const onSuccess = async ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId) return;
|
||||
cleanup();
|
||||
if (!lastImage) { tracker.emit({ phase: "error", message: "extend produced no clip" }); return; }
|
||||
const entry = await recordGeneration(workflowId, lastImage.filename, lastImage.subfolder, "output", {
|
||||
parent_filename: opts.parentFilename || "",
|
||||
});
|
||||
tracker.emit({ phase: "done", resultUrl: viewUrl(lastImage), resultHash: entry?.hash || null, entry });
|
||||
};
|
||||
const onError = ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId) return;
|
||||
cleanup();
|
||||
tracker.emit({ phase: "error", message: detail?.exception_message || "extend render failed" });
|
||||
};
|
||||
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);
|
||||
tracker.emit({ phase: "running" });
|
||||
})().catch((e) => {
|
||||
console.error("[Extend] background run failed", e);
|
||||
tracker.emit({ phase: "error", message: e?.message || "unexpected failure" });
|
||||
toast("error", `Extend failed: ${e?.message || e}`);
|
||||
});
|
||||
|
||||
return tracker;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// "Extend video" graph builder. Instantiates an I2V template (VERBATIM — its
|
||||
// lockModels recipe already matches the source clip's models) into an off-screen
|
||||
// LGraph and overrides ONLY the start image (the source video's last frame) and
|
||||
// the prompt, so the next clip continues from where the source ended. Mirrors
|
||||
// buildReposeGraph (repose-from-image.js). The last frame is staged server-side
|
||||
// by core/extend_api.py (/promptchain/extend/prepare).
|
||||
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { resolveInstalledVariant } from "./model-bridge.js";
|
||||
import { offscreenIdBase } from "./offscreen-graph.js";
|
||||
|
||||
const EMPTY_WORKFLOW = { last_node_id: 0, last_link_id: 0, nodes: [], links: [], groups: [], config: {}, extra: {}, version: 0.4 };
|
||||
|
||||
function freshWorkflowId() {
|
||||
return globalThis.crypto?.randomUUID ? crypto.randomUUID() : `${Date.now().toString(16)}-extend`;
|
||||
}
|
||||
|
||||
function setNamedWidget(node, name, value) {
|
||||
const w = node?.widgets?.find((x) => x.name === name);
|
||||
if (!w) return false;
|
||||
if (Array.isArray(w.options?.values) && value != null && !w.options.values.includes(value)) {
|
||||
w.options.values.push(value); // make a not-yet-listed file selectable
|
||||
}
|
||||
w.value = value;
|
||||
w.callback?.(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// opts: { templateId, lastFrameFilename, promptDoc, length }
|
||||
// Returns { graph, workflowId } or null.
|
||||
export async function buildExtendGraph(opts) {
|
||||
const LG = window.LiteGraph;
|
||||
if (!LG) return null;
|
||||
|
||||
let template;
|
||||
try {
|
||||
const res = await api.fetchApi(`/promptchain/templates/${encodeURIComponent(opts.templateId)}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
template = data?.template || data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!template?.nodes?.length) return null;
|
||||
|
||||
const workflowId = freshWorkflowId();
|
||||
const graph = new LG.LGraph(structuredClone({ ...EMPTY_WORKFLOW, id: workflowId }));
|
||||
graph.last_node_id = offscreenIdBase(); // render-node ids must stay off the live canvas — see offscreen-graph.js
|
||||
const created = new Array(template.nodes.length).fill(null);
|
||||
// Keeps the dual experts on one downloaded quant when remapping baked variants.
|
||||
const variantRef = { token: "" };
|
||||
|
||||
for (let i = 0; i < template.nodes.length; i++) {
|
||||
const tpl = template.nodes[i];
|
||||
const node = LG.createNode(tpl.type);
|
||||
if (!node) { console.warn(`[Extend] unknown node type ${tpl.type}`); return null; }
|
||||
graph.add(node);
|
||||
if (tpl.title) node.title = tpl.title;
|
||||
// Copy widget values verbatim — lockModels keeps the proven recipe (model /
|
||||
// LoRA / VAE / CLIP files, steps, cfg, shift). Only the pieces below vary.
|
||||
if (tpl.widgets_values?.length && node.widgets?.length) {
|
||||
for (let w = 0; w < Math.min(tpl.widgets_values.length, node.widgets.length); w++) {
|
||||
const widget = node.widgets[w];
|
||||
if (widget.type === "IMAGEUPLOAD" || widget.type === "button") continue;
|
||||
let val = tpl.widgets_values[w];
|
||||
// The template bakes a fixed GGUF quant / safetensors precision; remap it
|
||||
// to whichever variant of the same model file the user actually installed
|
||||
// (same fix applyTemplate does — buildExtendGraph doesn't route through it).
|
||||
if (typeof val === "string" && /\.(gguf|safetensors|ckpt)$/i.test(val)) {
|
||||
val = resolveInstalledVariant(val, widget.options?.values, variantRef);
|
||||
}
|
||||
if (Array.isArray(widget.options?.values) && val != null && !widget.options.values.includes(val)) {
|
||||
widget.options.values.push(val);
|
||||
}
|
||||
widget.value = val;
|
||||
widget.callback?.(val);
|
||||
}
|
||||
}
|
||||
created[i] = node;
|
||||
}
|
||||
|
||||
// ── overrides: start image = source's last frame; optional clip dims/length/
|
||||
// fps. Every override is OPTIONAL — an omitted opt keeps the template's tuned
|
||||
// value, so default Extend behaviour is unchanged. ──
|
||||
for (const node of created) {
|
||||
if (!node) continue;
|
||||
const type = node.comfyClass || node.type;
|
||||
if (type === "LoadImage") {
|
||||
setNamedWidget(node, "image", opts.lastFrameFilename);
|
||||
} else if (type === "WanImageToVideo" || type === "WanFirstLastFrameToVideo") {
|
||||
if (opts.length) setNamedWidget(node, "length", opts.length);
|
||||
if (opts.width) setNamedWidget(node, "width", opts.width);
|
||||
if (opts.height) setNamedWidget(node, "height", opts.height);
|
||||
} else if (type === "CreateVideo" && opts.outFps != null) {
|
||||
setNamedWidget(node, "fps", opts.outFps);
|
||||
}
|
||||
}
|
||||
|
||||
// ── advanced sampler overrides (all optional). The 14B recipe is DUAL-EXPERT:
|
||||
// two KSamplerAdvanced (the high-noise expert runs the first slice of the step
|
||||
// schedule, the low-noise expert the rest) + two ModelSamplingSD3. Identify the
|
||||
// high expert by its add_noise="enable" and preserve the template's proportional
|
||||
// step split so the MoE handoff stays intact; single-expert recipes (one
|
||||
// sampler) just take the full [0, steps] window. ──
|
||||
const widgetVal = (node, name) => node?.widgets?.find((w) => w.name === name)?.value;
|
||||
const ksamplers = created.filter((n) => n && (n.comfyClass || n.type) === "KSamplerAdvanced");
|
||||
const shiftNodes = created.filter((n) => n && (n.comfyClass || n.type) === "ModelSamplingSD3");
|
||||
const highK = ksamplers.find((n) => widgetVal(n, "add_noise") === "enable") || ksamplers[0] || null;
|
||||
const lowK = ksamplers.find((n) => n !== highK) || null;
|
||||
|
||||
if (opts.cfg != null) for (const k of ksamplers) setNamedWidget(k, "cfg", opts.cfg);
|
||||
if (opts.sampler) for (const k of ksamplers) setNamedWidget(k, "sampler_name", opts.sampler);
|
||||
if (opts.scheduler) for (const k of ksamplers) setNamedWidget(k, "scheduler", opts.scheduler);
|
||||
if (opts.shift != null) for (const m of shiftNodes) setNamedWidget(m, "shift", opts.shift);
|
||||
// Seed: a pinned seed fixes both experts; unpinned must inject a FRESH random
|
||||
// seed each run, because the off-screen api.queuePrompt path never fires the
|
||||
// "randomize" widget control — the baked noise_seed (0) would otherwise repeat
|
||||
// byte-identical every Extend / Run Again. The high expert drives the noise
|
||||
// (low has add_noise disabled), so randomizing it is enough.
|
||||
if (opts.seed != null) {
|
||||
for (const k of ksamplers) {
|
||||
setNamedWidget(k, "noise_seed", opts.seed);
|
||||
setNamedWidget(k, "control_after_generate", "fixed");
|
||||
}
|
||||
} else if (highK) {
|
||||
setNamedWidget(highK, "noise_seed", Math.floor(Math.random() * 2 ** 31));
|
||||
}
|
||||
if (opts.steps != null && highK) {
|
||||
const origSteps = Number(widgetVal(highK, "steps")) || opts.steps;
|
||||
const origEnd = Number(widgetVal(highK, "end_at_step"));
|
||||
setNamedWidget(highK, "steps", opts.steps);
|
||||
setNamedWidget(highK, "start_at_step", 0);
|
||||
if (lowK) {
|
||||
const ratio = origSteps > 0 && origEnd > 0 ? origEnd / origSteps : 0.5;
|
||||
const boundary = Math.max(1, Math.min(opts.steps - 1, Math.round(opts.steps * ratio)));
|
||||
setNamedWidget(highK, "end_at_step", boundary);
|
||||
setNamedWidget(lowK, "steps", opts.steps);
|
||||
setNamedWidget(lowK, "start_at_step", boundary);
|
||||
setNamedWidget(lowK, "end_at_step", opts.steps);
|
||||
} else {
|
||||
setNamedWidget(highK, "end_at_step", opts.steps);
|
||||
}
|
||||
}
|
||||
|
||||
// ── wiring ──
|
||||
for (const conn of template.connections || []) {
|
||||
const from = created[conn.from_node_idx];
|
||||
const to = created[conn.to_node_idx];
|
||||
if (from && to) {
|
||||
try { from.connect(conn.from_slot, to, conn.to_slot); } catch (e) { console.warn("[Extend] wire failed", e); }
|
||||
}
|
||||
}
|
||||
|
||||
// ── prompt: PromptChain_PromptChain wired via anchorConnections so the raw doc
|
||||
// compiles SERVER-SIDE (same engine repose/upscale use). Mirrors buildReposeGraph. ──
|
||||
const anchors = template.anchorConnections || [];
|
||||
if (anchors.length) {
|
||||
const pc = LG.createNode("PromptChain_PromptChain");
|
||||
if (!pc) { console.warn("[Extend] PromptChain_PromptChain unavailable (install the pack)"); return null; }
|
||||
graph.add(pc);
|
||||
const promptW = pc.widgets?.find((w) => w.name === "prompt");
|
||||
if (promptW) { promptW.value = opts.promptDoc || ""; promptW.callback?.(promptW.value); }
|
||||
pc.properties = pc.properties || {};
|
||||
Object.assign(pc.properties, {
|
||||
pcrMode: "switch", pcrSwitchIndex: 1, pcrLocked: false, pcrDisabled: false,
|
||||
pcrCachedOutput: "", pcrCachedNegOutput: "", pcrCachedRegions: "",
|
||||
});
|
||||
const maxSlot = Math.max(...anchors.map((c) => c.from_slot));
|
||||
if (!pc.outputs || pc.outputs.length <= maxSlot) {
|
||||
const ref = LG.createNode(pc.comfyClass || pc.type);
|
||||
if (ref?.outputs) {
|
||||
for (let i = pc.outputs?.length || 0; i < ref.outputs.length; i++) {
|
||||
pc.addOutput(ref.outputs[i].name, ref.outputs[i].type);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const conn of anchors) {
|
||||
const to = created[conn.to_node_idx];
|
||||
if (to) { try { pc.connect(conn.from_slot, to, conn.to_slot); } catch (e) { console.warn("[Extend] anchor wire failed", e); } }
|
||||
}
|
||||
}
|
||||
|
||||
// ── optional stitch: prepend the SOURCE clip so the result is ONE continuous
|
||||
// video. Load the source -> frames; drop the continuation's duplicate first
|
||||
// frame (ImageFromBatch from index 1 — it equals the source's last frame);
|
||||
// concatenate (source + trimmed continuation) and re-encode through the
|
||||
// template's own CreateVideo at the source fps. All core video/image nodes; if
|
||||
// any is missing we silently leave the continuation-only graph intact. ──
|
||||
// The continuation is native 16fps. A source can only be cleanly joined with it
|
||||
// when the source is also 16 or a whole-number RIFE multiple of 16 (32/48/64) —
|
||||
// those we either join directly or match by interpolating the continuation up.
|
||||
// A non-multiple source (24fps 5B, foreign 25/30/60) can't be reconciled without
|
||||
// frame-rate conversion, so we SKIP the stitch (save the continuation on its own)
|
||||
// rather than silently slow the source half by re-encoding it at 16.
|
||||
const NATIVE_FPS = 16;
|
||||
const srcFps = Math.round(opts.fps || NATIVE_FPS);
|
||||
const fpsMult = Math.round(srcFps / NATIVE_FPS);
|
||||
const cleanForStitch = srcFps === NATIVE_FPS || (fpsMult >= 2 && srcFps === fpsMult * NATIVE_FPS);
|
||||
if (opts.stitch && opts.sourceVideoFilename && !cleanForStitch) {
|
||||
console.warn(`[Extend] source ${srcFps}fps can't be cleanly joined with a 16fps continuation; saving continuation only`);
|
||||
}
|
||||
if (opts.stitch && opts.sourceVideoFilename && cleanForStitch) {
|
||||
const reg = LG.registered_node_types || {};
|
||||
const need = ["LoadVideo", "GetVideoComponents", "ImageBatch", "ImageFromBatch"];
|
||||
const vae = created.find((n) => n && (n.comfyClass || n.type) === "VAEDecode");
|
||||
const cv = created.find((n) => n && (n.comfyClass || n.type) === "CreateVideo");
|
||||
if (need.every((t) => reg[t]) && vae && cv) {
|
||||
const load = LG.createNode("LoadVideo"); graph.add(load); setNamedWidget(load, "file", opts.sourceVideoFilename);
|
||||
const comp = LG.createNode("GetVideoComponents"); graph.add(comp);
|
||||
const drop = LG.createNode("ImageFromBatch"); graph.add(drop);
|
||||
setNamedWidget(drop, "batch_index", 1);
|
||||
setNamedWidget(drop, "length", 4096); // clamps to "rest of the batch" (default 1 would keep one frame)
|
||||
const batch = LG.createNode("ImageBatch"); graph.add(batch);
|
||||
try {
|
||||
// Source is 16 or a clean RIFE multiple (32/48/64 — guaranteed by
|
||||
// cleanForStitch). When it's a multiple, bring the native-16 continuation
|
||||
// up to the same density first so both halves play at one rate; this is
|
||||
// what makes "extend an already-smoothed clip" just work. RIFE is present
|
||||
// whenever the source was smoothed.
|
||||
const matchInterp = fpsMult >= 2 && reg["RIFE VFI"];
|
||||
let contFrames = vae; // node feeding the continuation frames into the join
|
||||
if (matchInterp) {
|
||||
const rife = LG.createNode("RIFE VFI"); graph.add(rife);
|
||||
setNamedWidget(rife, "ckpt_name", "rife49.pth");
|
||||
setNamedWidget(rife, "multiplier", fpsMult);
|
||||
vae.connect(0, rife, 0); // continuation 16fps -> interpolate up to source density
|
||||
contFrames = rife;
|
||||
}
|
||||
load.connect(0, comp, 0); // source VIDEO -> GetVideoComponents.video
|
||||
contFrames.connect(0, drop, 0); // (matched) continuation frames -> drop frame 0
|
||||
comp.connect(0, batch, 0); // source frames -> ImageBatch.image1
|
||||
drop.connect(0, batch, 1); // trimmed continuation -> ImageBatch.image2
|
||||
batch.connect(0, cv, 0); // concatenated -> CreateVideo.images (replaces the VAEDecode link)
|
||||
// Encode at the source's rate when matched (both halves now that density),
|
||||
// else native 16 (the source was already 16). Never relabel un-matched
|
||||
// native frames to a higher fps — that's the fast-play bug.
|
||||
setNamedWidget(cv, "fps", matchInterp ? srcFps : NATIVE_FPS);
|
||||
} catch (e) {
|
||||
console.warn("[Extend] stitch wiring failed; producing continuation-only clip", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("[Extend] stitch requested but core video nodes unavailable; continuation-only");
|
||||
}
|
||||
}
|
||||
|
||||
return { graph, workflowId };
|
||||
}
|
||||
+79
-2
@@ -89,6 +89,39 @@ function ensureComboOption(widget, val) {
|
||||
}
|
||||
}
|
||||
|
||||
// A model filename is "<base>-<variant>.<ext>", where <variant> is a GGUF quant
|
||||
// (Q5_K_M, Q8_0, IQ2_XXS…) or a safetensors precision (fp8_scaled,
|
||||
// fp8_e4m3fn_scaled, fp16, bf16…). The base identifies the model file (e.g. a
|
||||
// Wan high-noise expert) independent of which variant was downloaded.
|
||||
const _VARIANT_RE = /^(.*)[-_]((?:Q\d+(?:_[0-9A-Za-z]+)*|IQ\d+[A-Za-z0-9_]*|fp8(?:_[a-z0-9]+)*|fp16|bf16|fp32|fp4|f16|f32)(?:_scaled)?)\.(gguf|safetensors|ckpt)$/i;
|
||||
|
||||
export function variantParts(name) {
|
||||
const m = _VARIANT_RE.exec(name || "");
|
||||
return m ? { base: m[1].toLowerCase(), token: m[2].toLowerCase(), ext: m[3].toLowerCase() } : null;
|
||||
}
|
||||
|
||||
// Templates bake one variant; the user may have downloaded another. Remap the
|
||||
// baked filename to whichever variant of the SAME model file is actually
|
||||
// installed (read from the loader widget's own combo list). `ref` carries the
|
||||
// variant chosen by a sibling loader so a multi-expert recipe (Wan high+low
|
||||
// noise) stays on a single quant when several are on disk. No-op when the baked
|
||||
// file is already installed or no same-base variant exists.
|
||||
export function resolveInstalledVariant(baked, installed, ref) {
|
||||
if (!Array.isArray(installed) || installed.includes(baked)) return baked;
|
||||
const bp = variantParts(baked);
|
||||
if (!bp) return baked;
|
||||
const candidates = installed.filter((o) => {
|
||||
const op = variantParts(o);
|
||||
return op && op.base === bp.base && op.ext === bp.ext;
|
||||
});
|
||||
if (!candidates.length) return baked;
|
||||
const pick = (ref?.token && candidates.find((o) => variantParts(o).token === ref.token))
|
||||
|| candidates.find((o) => o === baked)
|
||||
|| candidates[0];
|
||||
if (ref) ref.token = variantParts(pick).token;
|
||||
return pick;
|
||||
}
|
||||
|
||||
// Checkpoint-loadable archs all share CheckpointLoaderSimple, so they can be swapped
|
||||
// in place without rebuilding. Flux/Z-Image use different loaders → not swappable here.
|
||||
const CHECKPOINT_LOADERS = new Set(["CheckpointLoaderSimple", "CheckpointLoader"]);
|
||||
@@ -241,6 +274,9 @@ export function applyTemplate(pcNode, template, modelFilename) {
|
||||
? Math.max(0, (pcNode.size?.[0] || 0) + SPAWN_GAP - minOffsetX)
|
||||
: 0;
|
||||
|
||||
// Keeps multi-expert loaders (Wan high+low noise) on a single downloaded quant.
|
||||
const _variantRef = { token: "" };
|
||||
|
||||
const createdNodes = new Array(template.nodes.length).fill(null);
|
||||
for (let i = 0; i < template.nodes.length; i++) {
|
||||
const tplNode = template.nodes[i];
|
||||
@@ -254,6 +290,7 @@ export function applyTemplate(pcNode, template, modelFilename) {
|
||||
pcNode.pos[1] + tplNode.offset[1],
|
||||
];
|
||||
if (tplNode.size) newNode.size = [...tplNode.size];
|
||||
if (tplNode.title) newNode.title = tplNode.title;
|
||||
if (tplNode.properties) Object.assign(newNode.properties, tplNode.properties);
|
||||
graph.add(newNode);
|
||||
|
||||
@@ -266,13 +303,29 @@ export function applyTemplate(pcNode, template, modelFilename) {
|
||||
if (typeof val === "string" && val.includes("{model_name}")) {
|
||||
val = val.replace(/\{model_name\}/g, slugify(modelName));
|
||||
}
|
||||
// Remap a baked GGUF quant / safetensors precision to the variant the
|
||||
// user actually downloaded — fixes locked multi-expert templates (Wan)
|
||||
// pointing at a quant that isn't on disk. Reads the loader's installed
|
||||
// list before ensureComboOption appends the (possibly absent) baked name.
|
||||
if (typeof val === "string" && /\.(gguf|safetensors|ckpt)$/i.test(val)) {
|
||||
val = resolveInstalledVariant(val, newNode.widgets[w].options?.values, _variantRef);
|
||||
}
|
||||
ensureComboOption(newNode.widgets[w], val);
|
||||
newNode.widgets[w].value = val;
|
||||
newNode.widgets[w].callback?.(val);
|
||||
}
|
||||
}
|
||||
|
||||
if (template._presetNodes) {
|
||||
// A locked template is a self-contained recipe: it bakes both its model
|
||||
// filenames (see lockModels guard below) AND its sampler widget values.
|
||||
// The model config's node presets carry ONE recipe per family (e.g. Wan's
|
||||
// vanilla 20-step / CFG 3.5), which would clobber a sibling template's
|
||||
// distinct recipe — applying the vanilla preset over the Fast 4-step /
|
||||
// CFG 1.0 template left the LightX2V LoRAs running at CFG 3.5 and produced
|
||||
// burnt video. So skip preset overrides for locked templates; the model-
|
||||
// settings panel still reads slider RANGES from the config and current
|
||||
// values from the live (template-baked) nodes.
|
||||
if (template._presetNodes && !template.lockModels) {
|
||||
const nodeType = tplNode.type;
|
||||
const presetWidgets = template._presetNodes[nodeType];
|
||||
if (presetWidgets && newNode.widgets) {
|
||||
@@ -289,7 +342,11 @@ export function applyTemplate(pcNode, template, modelFilename) {
|
||||
createdNodes[i] = newNode;
|
||||
}
|
||||
|
||||
if (modelFilename) {
|
||||
// Single-loader templates inject the picked model into their loader. MoE/video
|
||||
// templates (e.g. Wan's high+low noise experts) set lockModels and keep their
|
||||
// own per-expert filenames — injecting one picked file would clobber the
|
||||
// second expert and break the pair.
|
||||
if (modelFilename && !template.lockModels) {
|
||||
const LOADER_TYPES = new Set([
|
||||
"CheckpointLoaderSimple", "CheckpointLoader",
|
||||
"UNETLoader", "DiffusersLoader",
|
||||
@@ -372,6 +429,13 @@ async function injectTemplatePrompt(pcNode, promptId) {
|
||||
const recipeHeader = text.split("\n").find((l) => l.trim()) || "";
|
||||
if (recipeHeader && docText.includes(recipeHeader)) return; // same template re-applied
|
||||
|
||||
// A recipe's "Negative Prompt:" section is the model's standard negative
|
||||
// (e.g. Wan's anti-static string). It sits below {cursor}, so the existing-
|
||||
// doc path below would drop it — capture it now to re-apply afterward.
|
||||
const NEG_RE = /(^|\n)[ \t]*negative\s+prompt\s*:/i;
|
||||
const negMatch = text.match(NEG_RE);
|
||||
const negSection = negMatch ? text.slice(negMatch.index).replace(/^\n/, "") : "";
|
||||
|
||||
const cursorIdx = text.indexOf("{cursor}");
|
||||
let insertText, anchor;
|
||||
if (!docText.trim()) {
|
||||
@@ -394,4 +458,17 @@ async function injectTemplatePrompt(pcNode, promptId) {
|
||||
} else if (promptWidget) {
|
||||
promptWidget.value = docText.trim() ? insertText + docText : insertText;
|
||||
}
|
||||
|
||||
// Existing prompt with no negative of its own: append the recipe's standard
|
||||
// negative (the empty-doc path already inserted it inline).
|
||||
if (negSection && docText.trim() && !NEG_RE.test(docText)) {
|
||||
if (view) {
|
||||
const end = view.state.doc.length;
|
||||
const tail = view.state.doc.toString().endsWith("\n") ? "" : "\n";
|
||||
view.dispatch({ changes: { from: end, to: end, insert: tail + negSection } });
|
||||
} else if (promptWidget) {
|
||||
const cur = String(promptWidget.value || "");
|
||||
promptWidget.value = cur + (cur.endsWith("\n") ? "" : "\n") + negSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,9 +233,9 @@ async function showModelPickerInner(pcNode, anchorEl, opts) {
|
||||
closeModelPicker();
|
||||
showDownloadModal(civitaiResult, pcNode, anchorEl);
|
||||
},
|
||||
onShowCatalogDownload: (catalogEntry) => {
|
||||
onShowCatalogDownload: (catalogEntry, initialPrecision) => {
|
||||
closeModelPicker();
|
||||
showCatalogDownloadModal(catalogEntry, pcNode, anchorEl);
|
||||
showCatalogDownloadModal(catalogEntry, pcNode, anchorEl, initialPrecision);
|
||||
},
|
||||
onClose: () => closeModelPicker(),
|
||||
applyTemplate: (tpl, filename, presetNodes) => {
|
||||
@@ -304,7 +304,7 @@ function closeCatalogDownloadModal() {
|
||||
}
|
||||
}
|
||||
|
||||
async function showCatalogDownloadModal(catalogEntry, pcNode, indicatorEl) {
|
||||
async function showCatalogDownloadModal(catalogEntry, pcNode, indicatorEl, initialPrecision = null) {
|
||||
closeCatalogDownloadModal();
|
||||
ensureCSS();
|
||||
|
||||
@@ -315,6 +315,7 @@ async function showCatalogDownloadModal(catalogEntry, pcNode, indicatorEl) {
|
||||
|
||||
_activeCatalogDownload = mod.mountCatalogDownloadModal(_catalogContainer, {
|
||||
catalogEntry,
|
||||
initialPrecision,
|
||||
onClose: () => closeCatalogDownloadModal(),
|
||||
onModelReady: (filename) => {
|
||||
closeCatalogDownloadModal();
|
||||
|
||||
@@ -31,7 +31,7 @@ export const FAMILIES = {
|
||||
zimage: [{ id: "zimage_base", label: "Z-Image Base" }, { id: "zimage_turbo", label: "Z-Image Turbo" }],
|
||||
qwen_image: [{ id: "qwen_image", label: "Qwen Image" }],
|
||||
qwen_edit: [{ id: "qwen_edit", label: "Qwen Edit" }, { id: "qwen_aio", label: "Qwen AIO" }],
|
||||
wan22: [{ id: "wan22_t2v", label: "Wan T2V 14B" }, { id: "wan22_i2v", label: "Wan I2V 14B" }, { id: "wan22_5b", label: "Wan 5B" }],
|
||||
wan22: [{ id: "wan22_i2v", label: "Wan I2V 14B" }, { id: "wan22_i2v_gguf", label: "Wan I2V 14B (GGUF)" }, { id: "wan22_t2v", label: "Wan T2V 14B" }, { id: "wan22_t2v_gguf", label: "Wan T2V 14B (GGUF)" }, { id: "wan22_5b", label: "Wan TI2V 5B" }, { id: "wan22_5b_gguf", label: "Wan TI2V 5B (GGUF)" }],
|
||||
ltx: [{ id: "ltx23", label: "LTX 2.3" }],
|
||||
hunyuan_video: [{ id: "hunyuan_video_15", label: "HunyuanVideo 1.5" }],
|
||||
hidream: [{ id: "hidream", label: "HiDream-I1" }],
|
||||
@@ -215,7 +215,37 @@ export const FLUX_NODES = {
|
||||
},
|
||||
};
|
||||
|
||||
// Ideogram 4 tuning nodes. Its t2i graph samples through PromptChain_IdeogramSampler
|
||||
// (a SamplerCustomAdvanced+VAEDecode drop-in), fed by these. Surfaced in the model
|
||||
// Settings panel like FLUX_NODES so an Ideogram model has real controls instead of
|
||||
// an empty pane. Resolution stays on the EmptyFlux2LatentImage row (LATENT_NODES);
|
||||
// the scheduler's own width/height are intentionally not exposed here to avoid a
|
||||
// second resolution row.
|
||||
export const IDEOGRAM_NODES = {
|
||||
Ideogram4Scheduler: {
|
||||
label: "Scheduler",
|
||||
widgets: {
|
||||
steps: { type: "slider", min: 1, max: 200, step: 1 },
|
||||
},
|
||||
},
|
||||
DualModelGuider: {
|
||||
label: "Guidance",
|
||||
widgets: {
|
||||
cfg: { type: "slider", min: 0, max: 30, step: 0.1 },
|
||||
},
|
||||
},
|
||||
CFGOverride: {
|
||||
label: "CFG Override",
|
||||
widgets: {
|
||||
cfg: { type: "slider", min: 0, max: 30, step: 0.1 },
|
||||
start_percent: { type: "slider", min: 0, max: 1, step: 0.01 },
|
||||
end_percent: { type: "slider", min: 0, max: 1, step: 0.01 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const FLUX_NODE_TYPES = new Set(Object.keys(FLUX_NODES));
|
||||
const IDEOGRAM_NODE_TYPES = new Set(Object.keys(IDEOGRAM_NODES));
|
||||
const SAMPLER_INCLUDES = ["KSampler"];
|
||||
const LATENT_NODE_TYPES = new Set(Object.keys(LATENT_NODES));
|
||||
const CLIP_NODE_TYPES = new Set(Object.keys(CLIP_NODES));
|
||||
@@ -540,6 +570,7 @@ export function detectSupportedNodes(pcNode) {
|
||||
const seenIds = new Set();
|
||||
let ksamplerNode = null;
|
||||
let fluxSamplerNode = null;
|
||||
let ideogramSamplerNode = null;
|
||||
|
||||
for (const node of downstream) {
|
||||
if (seenIds.has(node.id)) continue;
|
||||
@@ -551,6 +582,16 @@ export function detectSupportedNodes(pcNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ideogram samples through PromptChain_IdeogramSampler (a SamplerCustomAdvanced
|
||||
// drop-in). Reuse the Flux pivot machinery (latent/sampler/noise upstream walk)
|
||||
// and additionally walk the Ideogram-only tuning nodes below.
|
||||
if (cls === "PromptChain_IdeogramSampler") {
|
||||
seenIds.add(node.id);
|
||||
ideogramSamplerNode = node;
|
||||
fluxSamplerNode = node;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ControlNet branches: one deferred section per branch, keyed by control
|
||||
// type (depth/canny/…) so ranges + recommended don't collide when stacked.
|
||||
if (cls === "ControlNetApplyAdvanced") {
|
||||
@@ -575,7 +616,9 @@ export function detectSupportedNodes(pcNode) {
|
||||
continue;
|
||||
}
|
||||
for (const pattern of SAMPLER_INCLUDES) {
|
||||
if (cls.includes(pattern)) {
|
||||
// KSamplerSelect just PICKS a sampler (outputs SAMPLER); it's not a real
|
||||
// sampler pivot — exclude it so it never gets adopted as the latent anchor.
|
||||
if (cls.includes(pattern) && cls !== "KSamplerSelect") {
|
||||
seenIds.add(node.id);
|
||||
detected.push({ node, config: SUPPORTED_NODES.KSampler, type: cls });
|
||||
ksamplerNode = node;
|
||||
@@ -594,6 +637,19 @@ export function detectSupportedNodes(pcNode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ideogram-only tuning nodes upstream of the Ideogram sampler (steps / guidance /
|
||||
// cfg-override). Resolution + sampler + noise are already picked up via the Flux
|
||||
// walk above and the latent walk below.
|
||||
if (ideogramSamplerNode) {
|
||||
for (const cls of IDEOGRAM_NODE_TYPES) {
|
||||
const n = findUpstreamByType(ideogramSamplerNode, new Set([cls]));
|
||||
if (n && !seenIds.has(n.id)) {
|
||||
seenIds.add(n.id);
|
||||
detected.push({ node: n, config: IDEOGRAM_NODES[cls], type: cls });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pivotNode = ksamplerNode || fluxSamplerNode;
|
||||
if (pivotNode) {
|
||||
if (!fluxSamplerNode) {
|
||||
@@ -2348,7 +2404,7 @@ export function resolveResolutionTicks(detected) {
|
||||
}
|
||||
if (!inputLongest) {
|
||||
const latEntry = detected.find(d =>
|
||||
d.type === "EmptyLatentImage" || d.type === "EmptySD3LatentImage"
|
||||
d.type === "EmptyLatentImage" || d.type === "EmptySD3LatentImage" || d.type === "EmptyFlux2LatentImage"
|
||||
);
|
||||
if (latEntry) {
|
||||
const wW = readWidgetValue(latEntry.node, "width");
|
||||
@@ -2728,9 +2784,9 @@ export async function showModelModal(pcNode, modelInfo, anchorEl, { tab = null }
|
||||
}
|
||||
return items;
|
||||
},
|
||||
onShowCatalogDownload: (entry) => {
|
||||
onShowCatalogDownload: (entry, initialPrecision) => {
|
||||
closeModelModal();
|
||||
showCatalogDownloadModal(entry, pcNode, anchorEl);
|
||||
showCatalogDownloadModal(entry, pcNode, anchorEl, initialPrecision);
|
||||
},
|
||||
// Editor reference for prompts tab
|
||||
getEditor: () => pcNode._pcrEditor,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Shared helper for off-screen render graphs (extend / smooth / repose).
|
||||
//
|
||||
// ComfyUI keys execution outputs by node id GLOBALLY: app.ts runs
|
||||
// nodeOutputStore.setNodeOutputsByExecutionId(String(detail.node), output)
|
||||
// and then paints that output onto whatever node in the VISIBLE graph carries
|
||||
// the same id. A background render graph numbered from 1 (EMPTY_WORKFLOW starts
|
||||
// at last_node_id: 0) therefore collides with low-id live nodes — e.g. a 5-node
|
||||
// Smooth graph whose SaveVideo is id 5 lands its broken-looking video frame on
|
||||
// the user's PromptChain node when that node also happens to be id 5. Numbering
|
||||
// our render nodes far above every live id keeps execution results off canvas.
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
const OFFSCREEN_GAP = 1_000_000;
|
||||
|
||||
// First id to assign in an off-screen graph: above every id the live canvas
|
||||
// could report, so ComfyUI's id-keyed output painter never finds a visible
|
||||
// match. Set graph.last_node_id to this BEFORE adding nodes (LGraph.add then
|
||||
// hands out base+1, base+2, …).
|
||||
export function offscreenIdBase() {
|
||||
let maxLiveId = 0;
|
||||
try {
|
||||
for (const g of [app?.graph, app?.rootGraph, app?.canvas?.graph]) {
|
||||
const n = Number(g?.last_node_id);
|
||||
if (Number.isFinite(n) && n > maxLiveId) maxLiveId = n;
|
||||
}
|
||||
} catch { /* app not ready → the constant gap alone is collision-proof */ }
|
||||
return maxLiveId + OFFSCREEN_GAP;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Shared registry of live Region Box (PromptChain_RegionBox) nodes.
|
||||
//
|
||||
// region-box.js populates this as Region Box nodes mount/unmount; main.js reads
|
||||
// it so each PromptChain node's menubar can gate the "pop the Region Box into a
|
||||
// panel" button (enabled only when a Region Box exists) and so the panel knows
|
||||
// which node to dock. A single shared module instead of a window global keeps it
|
||||
// deterministic regardless of which extension file the loader runs first.
|
||||
//
|
||||
// Twin of pose-registry.js — separate so the two panels gate independently.
|
||||
// Event-driven: subscribers are notified on add/remove (never polled).
|
||||
|
||||
const nodes = new Set();
|
||||
const listeners = new Set();
|
||||
let lastActive = null;
|
||||
|
||||
function notify() {
|
||||
for (const cb of listeners) {
|
||||
try { cb(); } catch (e) { console.error("[PromptChain] region-box-registry listener error", e); }
|
||||
}
|
||||
}
|
||||
|
||||
export const regionBoxRegistry = {
|
||||
// Called by region-box.js on mount.
|
||||
add(node) {
|
||||
nodes.add(node);
|
||||
lastActive = node;
|
||||
notify();
|
||||
},
|
||||
// Called by region-box.js on teardown.
|
||||
remove(node) {
|
||||
const had = nodes.delete(node);
|
||||
if (lastActive === node) lastActive = null;
|
||||
if (had) notify();
|
||||
},
|
||||
// Called when a Region Box canvas is interacted with, so "which node does the
|
||||
// panel dock" follows the one you last touched. Quiet by design — switching
|
||||
// focus shouldn't yank an already-open panel onto a different node.
|
||||
touch(node) {
|
||||
if (nodes.has(node)) lastActive = node;
|
||||
},
|
||||
// The canvas finished mounting (enterDock now exists). add() fired its notify
|
||||
// earlier; re-notify so an already-open panel can dock now.
|
||||
signalReady(node) {
|
||||
if (nodes.has(node)) notify();
|
||||
},
|
||||
all() {
|
||||
return [...nodes];
|
||||
},
|
||||
// The node a freshly-opened panel should host: last-interacted if still alive,
|
||||
// else any live one.
|
||||
getActive() {
|
||||
if (lastActive && lastActive._pcrAlive && nodes.has(lastActive)) return lastActive;
|
||||
for (const n of nodes) if (n._pcrAlive) return n;
|
||||
return null;
|
||||
},
|
||||
get count() { return nodes.size; },
|
||||
// Returns an unsubscribe fn. Fires on add/remove (gating + open-panel refresh).
|
||||
subscribe(cb) {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
// Shared region-name binding between a region-mask PRODUCER (the 3D Poser, the
|
||||
// Region Box canvas) and a regional CONSUMER (Attention Couple / Regional
|
||||
// Conditioning) wired to a PromptChain node.
|
||||
//
|
||||
// A producer emits MASKS (one row per region entity, in entity order) plus a
|
||||
// POSE_JSON entity list; a consumer binds each `$name{}` block in the prompt to
|
||||
// its mask row BY NAME. These helpers keep the bound prompt stocked with one
|
||||
// `$name{}` block per producer region and travel renames into the prompt.
|
||||
//
|
||||
// Generalized fork of the Poser's own (AttentionCouple-only) helpers so the Flux
|
||||
// RegionalConditioning path binds region names too, and so the seed budget is
|
||||
// per-producer (counting only the caller's own names) — the Poser's inline copy
|
||||
// is intentionally left untouched.
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { getLink } from "./slot-utils.js";
|
||||
import { setEditorContent } from "./editor.js";
|
||||
|
||||
// PromptChain consumers that take a `regions` JSON and bind each $name{} block
|
||||
// to a region. AttentionCouple patches SDXL cross-attention; RegionalConditioning
|
||||
// attaches the mask inside the conditioning (Flux + tiled upscale); IdeogramCaption
|
||||
// places each region at its box's caption coordinates (Ideogram has no mask, so it
|
||||
// reads the box from POSE_JSON, not MASKS). A producer feeds any of these.
|
||||
export const REGIONAL_CONSUMERS = new Set([
|
||||
"PromptChain_AttentionCouple",
|
||||
"PromptChain_RegionalConditioning",
|
||||
"PromptChain_IdeogramCaption",
|
||||
]);
|
||||
|
||||
// Producer outputs that can reach a consumer: MASKS for the mask consumers,
|
||||
// POSE_JSON for the Ideogram caption (mask-less).
|
||||
const PRODUCER_OUTPUTS = ["MASKS", "POSE_JSON"];
|
||||
|
||||
// $-block names are matched as \w+ by the compiler; drop any other char so a
|
||||
// stray metachar in a label can't corrupt the seed/rename regexes. (Labels are
|
||||
// already constrained to word-chars at the canvas, so this is a safety net.)
|
||||
function safeName(name) {
|
||||
return String(name || "").replace(/[^\w]/g, "");
|
||||
}
|
||||
|
||||
// Trace a producer node's MASKS/POSE_JSON output -> a regional consumer -> its
|
||||
// `regions` input -> the PromptChain node feeding it. Returns { consumer, pc }
|
||||
// or {}.
|
||||
export function traceRegionConsumer(node) {
|
||||
const graph = app.graph;
|
||||
if (!graph) return {};
|
||||
for (const outName of PRODUCER_OUTPUTS) {
|
||||
const out = node.outputs?.find((o) => o.name === outName);
|
||||
if (!out?.links?.length) continue;
|
||||
for (const linkId of out.links) {
|
||||
const link = getLink(graph, linkId);
|
||||
const consumer = link && graph.getNodeById(link.target_id);
|
||||
if (!REGIONAL_CONSUMERS.has(consumer?.comfyClass)) continue;
|
||||
const regIn = consumer.inputs?.find((i) => i.name === "regions");
|
||||
const rlink = regIn?.link != null ? getLink(graph, regIn.link) : null;
|
||||
const pc = rlink && graph.getNodeById(rlink.origin_id);
|
||||
if (pc?.comfyClass === "PromptChain_PromptChain") return { consumer, pc };
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Seed a `$name { }` block into the bound prompt for every region name that has
|
||||
// no block yet (add-only, case-insensitive to match the server's name binding).
|
||||
// Per-producer by construction: it only ever seeds the names passed in, so a
|
||||
// Poser sharing the same prompt can't make it over/under-seed. Inserts ABOVE the
|
||||
// negative section so a new block never lands inside the negative prompt.
|
||||
export function reconcileRegionBlocks(node, names) {
|
||||
if (!names?.length) return;
|
||||
const { pc } = traceRegionConsumer(node);
|
||||
if (!pc?._pcrEditor) return;
|
||||
const text = pc._pcrEditor.state.doc.toString();
|
||||
const missing = names.filter(
|
||||
(n) => safeName(n) && !new RegExp("\\$" + safeName(n) + "\\s*\\{", "i").test(text));
|
||||
if (!missing.length) return;
|
||||
const blocks = missing.map((name) => "$" + safeName(name) + " {\n\n}").join("\n\n");
|
||||
const neg = text.match(/negative\s+prompt\s*:/i);
|
||||
if (neg) {
|
||||
const head = text.slice(0, neg.index);
|
||||
setEditorContent(pc._pcrEditor,
|
||||
(head.trim() ? head.replace(/\s*$/, "\n\n") : "") + blocks + "\n\n" + text.slice(neg.index));
|
||||
} else {
|
||||
setEditorContent(pc._pcrEditor, text + (text.trim() ? "\n\n" : "") + blocks);
|
||||
}
|
||||
}
|
||||
|
||||
// Read a `$name { body }` block's body to its MATCHING close brace (brace-depth
|
||||
// tracked), skipping past any EMPTY same-name blocks to the first POPULATED one.
|
||||
// Depth tracking keeps a body's own {a|b} wildcard/alternation braces intact, so
|
||||
// what the user typed survives the copy — the server's compile grammar is itself
|
||||
// non-greedy, but preserving the authored text is strictly safer than truncating
|
||||
// it here. Returns "" when the name has no populated block.
|
||||
function extractRegionBody(text, sn) {
|
||||
const open = new RegExp("\\$" + sn + "\\s*\\{", "gi");
|
||||
let m;
|
||||
while ((m = open.exec(text)) !== null) {
|
||||
let depth = 1, i = m.index + m[0].length;
|
||||
const start = i;
|
||||
for (; i < text.length && depth > 0; i++) {
|
||||
if (text[i] === "{") depth++;
|
||||
else if (text[i] === "}") depth--;
|
||||
}
|
||||
if (depth === 0) {
|
||||
const body = text.slice(start, i - 1).trim();
|
||||
if (body) return body;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Read each named region's typed body out of the bound prompt, so a copied
|
||||
// character's prompt can travel with it to another scene. Null-prototype map so a
|
||||
// region literally named "__proto__" can't poison the result. Keyed by the name
|
||||
// AS PASSED (the entity's custom name), not the escaped form.
|
||||
export function readRegionBlockBodies(node, names) {
|
||||
const out = Object.create(null);
|
||||
if (!names?.length) return out;
|
||||
const { pc } = traceRegionConsumer(node);
|
||||
if (!pc?._pcrEditor) return out;
|
||||
const text = pc._pcrEditor.state.doc.toString();
|
||||
for (const name of names) {
|
||||
const sn = safeName(name);
|
||||
if (!sn) continue;
|
||||
const body = extractRegionBody(text, sn);
|
||||
if (body) out[name] = body;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Seed `$name { body }` blocks carrying the typed tags of region entities pasted
|
||||
// from another scene — like reconcileRegionBlocks but writes each block's BODY
|
||||
// (not an empty stub) and is keyed per pasted entity, so a copied character's
|
||||
// prompt travels with it. Add-only/case-insensitive: never clobbers a block the
|
||||
// destination already holds under that name. Returns true once the bound editor
|
||||
// exists (injection done, or nothing fresh to add) so the caller can stop
|
||||
// retrying; false while the editor isn't built yet (caller keeps it pending).
|
||||
export function seedRegionBlocks(node, entries) {
|
||||
if (!entries?.length) return true;
|
||||
const { pc } = traceRegionConsumer(node);
|
||||
if (!pc) return true; // not region-wired → nothing to inject into; caller must stop retrying
|
||||
if (!pc._pcrEditor) return false; // consumer wired but its editor isn't built yet → retry next capture
|
||||
const text = pc._pcrEditor.state.doc.toString();
|
||||
const fresh = entries.filter(
|
||||
(e) => safeName(e.name) && !new RegExp("\\$" + safeName(e.name) + "\\s*\\{", "i").test(text));
|
||||
if (!fresh.length) return true;
|
||||
const blocks = fresh.map((e) => {
|
||||
const body = (e.body || "").trim();
|
||||
return "$" + safeName(e.name) + " {\n" + (body ? body + "\n" : "\n") + "}";
|
||||
}).join("\n\n");
|
||||
// Carried region blocks LEAD the prompt: prepend at the very top so a pasted
|
||||
// character's $name{ tags } is the first thing in the prompt (above the quality
|
||||
// tags). Top placement is trivially above the negative section, so the
|
||||
// negative-marker guard reconcileRegionBlocks needs isn't required here.
|
||||
const rest = text.replace(/^\s+/, "");
|
||||
setEditorContent(pc._pcrEditor, blocks + (rest ? "\n\n" + rest : ""));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rewrite every `$oldName {` block to `$newName {` so the user's typed tags
|
||||
// travel with a box rename. No-op when no such block exists (reconcile seeds the
|
||||
// new name instead on the next change).
|
||||
export function renameRegionBlock(node, oldName, newName) {
|
||||
const oldN = safeName(oldName), newN = safeName(newName);
|
||||
if (!oldN || !newN || oldN === newN) return;
|
||||
const { pc } = traceRegionConsumer(node);
|
||||
if (!pc?._pcrEditor) return;
|
||||
const text = pc._pcrEditor.state.doc.toString();
|
||||
const re = new RegExp("\\$" + oldN + "(\\s*\\{)", "gi");
|
||||
if (!re.test(text)) return;
|
||||
re.lastIndex = 0;
|
||||
setEditorContent(pc._pcrEditor, text.replace(re, (m, brace) => "$" + newN + brace));
|
||||
}
|
||||
|
||||
// Region->entity binding is by NAME on the server, which needs the entity list —
|
||||
// wire POSE_JSON into the consumer's `pose` input if it isn't already. Connects
|
||||
// ONLY when the slot is empty: never steals `pose` from a 3D Poser that already
|
||||
// owns it (whoever owns `pose` defines the entity list for the whole prompt).
|
||||
export function ensureConsumerPoseWire(node) {
|
||||
const { consumer } = traceRegionConsumer(node);
|
||||
if (!consumer) return;
|
||||
const poseIn = consumer.inputs?.findIndex((i) => i.name === "pose");
|
||||
if (poseIn == null || poseIn < 0 || consumer.inputs[poseIn].link != null) return;
|
||||
const poseOut = node.outputs?.findIndex((o) => o.name === "POSE_JSON");
|
||||
if (poseOut >= 0) node.connect(poseOut, consumer, poseIn);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { REPOSE_RECIPES } from "./repose-recipes.js";
|
||||
import { offscreenIdBase } from "./offscreen-graph.js";
|
||||
|
||||
const EMPTY_WORKFLOW = { last_node_id: 0, last_link_id: 0, nodes: [], links: [], groups: [], config: {}, extra: {}, version: 0.4 };
|
||||
|
||||
@@ -123,6 +124,7 @@ export async function buildReposeGraph(opts) {
|
||||
|
||||
const workflowId = freshWorkflowId();
|
||||
const graph = new LG.LGraph(structuredClone({ ...EMPTY_WORKFLOW, id: workflowId }));
|
||||
graph.last_node_id = offscreenIdBase(); // render-node ids must stay off the live canvas — see offscreen-graph.js
|
||||
|
||||
const created = new Array(template.nodes.length).fill(null);
|
||||
let poserLoader = null;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// "Smooth" background runner — RIFE interpolation off-screen, queued, recorded as
|
||||
// a lineage CHILD of the source video. Mirrors extend-background.js.
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { recordGeneration, externallyTrackedPrompts, armExternalQueue, disarmExternalQueue } from "./history.js";
|
||||
import { buildSmoothGraph } from "./smooth-from-image.js";
|
||||
|
||||
function toast(severity, detail, life = 8000) {
|
||||
app.extensionManager?.toast?.add({ severity, summary: "Smooth", detail, life });
|
||||
}
|
||||
|
||||
function createTracker() {
|
||||
const listeners = new Set();
|
||||
const tracker = {
|
||||
state: { phase: "building" },
|
||||
subscribe(fn) { listeners.add(fn); fn(tracker.state); return () => listeners.delete(fn); },
|
||||
emit(state) { tracker.state = state; for (const fn of listeners) { try { fn(state); } catch {} } },
|
||||
};
|
||||
return tracker;
|
||||
}
|
||||
|
||||
function viewUrl(img) {
|
||||
if (!img?.filename) return "";
|
||||
const q = new URLSearchParams({ filename: img.filename, subfolder: img.subfolder || "", type: img.type || "output" });
|
||||
return api.apiURL ? api.apiURL(`/view?${q}`) : `/view?${q}`;
|
||||
}
|
||||
|
||||
// opts: { sourceVideoFilename, multiplier, outFps, parentFilename }
|
||||
export function runSmoothInBackground(opts) {
|
||||
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" });
|
||||
toast("info", "Smooth 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("[Smooth] cancel request failed", e); }
|
||||
};
|
||||
|
||||
(async () => {
|
||||
tracker.emit({ phase: "building" });
|
||||
// Whole opts through — an allowlist here dropped the Advanced knobs
|
||||
// (ckpt/scale_factor/ensemble/fast_mode) the modal sends.
|
||||
const built = buildSmoothGraph({ ...opts });
|
||||
if (!built) { tracker.emit({ phase: "error", message: "couldn't build the smooth graph (is the RIFE pack installed?)" }); return; }
|
||||
const { graph, workflowId } = built;
|
||||
if (run.cancelled) return;
|
||||
|
||||
tracker.emit({ phase: "queueing" });
|
||||
const serialized = await app.graphToPrompt(graph);
|
||||
armExternalQueue();
|
||||
let promptId = null;
|
||||
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: serialized.output, workflow: serialized.workflow }, { previewMethod });
|
||||
promptId = res?.prompt_id || null;
|
||||
} catch (e) {
|
||||
disarmExternalQueue();
|
||||
const message = e?.response?.error?.message || e?.message || "queue rejected the prompt";
|
||||
tracker.emit({ phase: "error", message });
|
||||
toast("error", `Smooth failed to queue: ${message}`);
|
||||
return;
|
||||
}
|
||||
if (promptId) externallyTrackedPrompts.add(promptId);
|
||||
disarmExternalQueue();
|
||||
run.promptId = promptId;
|
||||
if (!promptId) { tracker.emit({ phase: "error", message: "queue returned no prompt id" }); return; }
|
||||
if (run.cancelled) {
|
||||
try {
|
||||
await api.fetchApi("/queue", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: [promptId] }) });
|
||||
} catch {}
|
||||
externallyTrackedPrompts.delete(promptId);
|
||||
return;
|
||||
}
|
||||
|
||||
let lastImage = null, lastPreview = null;
|
||||
const onStart = ({ detail }) => { if (detail?.prompt_id === promptId) run.executionStarted = true; };
|
||||
const onProgress = ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId || run.cancelled) return;
|
||||
run.executionStarted = true;
|
||||
tracker.emit({ ...tracker.state, phase: "running", value: detail?.value || 0, max: detail?.max || 0 });
|
||||
};
|
||||
const onPreview = ({ detail }) => {
|
||||
if (run.cancelled || !run.executionStarted || !(detail instanceof Blob)) return;
|
||||
if (lastPreview) URL.revokeObjectURL(lastPreview);
|
||||
lastPreview = URL.createObjectURL(detail);
|
||||
tracker.emit({ ...tracker.state, phase: "running", previewUrl: lastPreview });
|
||||
};
|
||||
const onExecuted = ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId || !detail?.output?.images?.length) return;
|
||||
lastImage = detail.output.images[detail.output.images.length - 1];
|
||||
};
|
||||
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);
|
||||
};
|
||||
run.cleanup = cleanup;
|
||||
const onSuccess = async ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId) return;
|
||||
cleanup();
|
||||
if (!lastImage) { tracker.emit({ phase: "error", message: "smooth produced no video" }); return; }
|
||||
const entry = await recordGeneration(workflowId, lastImage.filename, lastImage.subfolder, "output", {
|
||||
parent_filename: opts.parentFilename || "",
|
||||
});
|
||||
tracker.emit({ phase: "done", resultUrl: viewUrl(lastImage), resultHash: entry?.hash || null, entry });
|
||||
};
|
||||
const onError = ({ detail }) => {
|
||||
if (detail?.prompt_id !== promptId) return;
|
||||
cleanup();
|
||||
tracker.emit({ phase: "error", message: detail?.exception_message || "smooth render failed" });
|
||||
};
|
||||
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);
|
||||
tracker.emit({ phase: "running" });
|
||||
})().catch((e) => {
|
||||
console.error("[Smooth] background run failed", e);
|
||||
tracker.emit({ phase: "error", message: e?.message || "unexpected failure" });
|
||||
toast("error", `Smooth failed: ${e?.message || e}`);
|
||||
});
|
||||
|
||||
return tracker;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// "Smooth" graph builder — RIFE frame interpolation as a viewer post-process.
|
||||
// Builds an off-screen graph: LoadVideo -> GetVideoComponents -> RIFE VFI ->
|
||||
// CreateVideo (fps x multiplier) -> SaveVideo. All core ComfyUI video nodes
|
||||
// except RIFE VFI (Fannovel16 ComfyUI-Frame-Interpolation, bundled as the
|
||||
// "FrameInterpolation" pack). The source video is staged in input/ by
|
||||
// /promptchain/extend/prepare (reused) so LoadVideo can consume it.
|
||||
|
||||
import { offscreenIdBase } from "./offscreen-graph.js";
|
||||
|
||||
const EMPTY_WORKFLOW = { last_node_id: 0, last_link_id: 0, nodes: [], links: [], groups: [], config: {}, extra: {}, version: 0.4 };
|
||||
|
||||
function freshWorkflowId() {
|
||||
return globalThis.crypto?.randomUUID ? crypto.randomUUID() : `${Date.now().toString(16)}-smooth`;
|
||||
}
|
||||
|
||||
function setNamedWidget(node, name, value) {
|
||||
const w = node?.widgets?.find((x) => x.name === name);
|
||||
if (!w) return false;
|
||||
if (Array.isArray(w.options?.values) && value != null && !w.options.values.includes(value)) {
|
||||
w.options.values.push(value); // a freshly-staged input video isn't in the combo yet
|
||||
}
|
||||
w.value = value;
|
||||
w.callback?.(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// True only when every node the Smooth graph needs is registered (RIFE pack +
|
||||
// core video nodes). The caller gates the install offer on RIFE VFI specifically.
|
||||
export function smoothNodesAvailable() {
|
||||
const reg = window.LiteGraph?.registered_node_types || {};
|
||||
return ["RIFE VFI", "LoadVideo", "GetVideoComponents", "CreateVideo", "SaveVideo"].every((t) => reg[t]);
|
||||
}
|
||||
|
||||
// opts: { sourceVideoFilename, multiplier, outFps }
|
||||
export function buildSmoothGraph(opts) {
|
||||
const LG = window.LiteGraph;
|
||||
if (!LG || !smoothNodesAvailable()) return null;
|
||||
|
||||
const workflowId = freshWorkflowId();
|
||||
const graph = new LG.LGraph(structuredClone({ ...EMPTY_WORKFLOW, id: workflowId }));
|
||||
graph.last_node_id = offscreenIdBase(); // render-node ids must stay off the live canvas — see offscreen-graph.js
|
||||
const mk = (t) => { const n = LG.createNode(t); if (n) graph.add(n); return n; };
|
||||
|
||||
const load = mk("LoadVideo");
|
||||
const comp = mk("GetVideoComponents");
|
||||
const rife = mk("RIFE VFI");
|
||||
const create = mk("CreateVideo");
|
||||
const save = mk("SaveVideo");
|
||||
if (!load || !comp || !rife || !create || !save) return null;
|
||||
|
||||
setNamedWidget(load, "file", opts.sourceVideoFilename);
|
||||
setNamedWidget(rife, "ckpt_name", opts.ckptName || "rife49.pth");
|
||||
setNamedWidget(rife, "multiplier", opts.multiplier || 2);
|
||||
// Quality/VRAM knobs — all optional, RIFE's own defaults if omitted. A picked
|
||||
// ckpt that isn't on disk auto-downloads on first run (the node handles it).
|
||||
if (opts.scaleFactor != null) setNamedWidget(rife, "scale_factor", opts.scaleFactor);
|
||||
if (opts.ensemble != null) setNamedWidget(rife, "ensemble", opts.ensemble);
|
||||
if (opts.fastMode != null) setNamedWidget(rife, "fast_mode", opts.fastMode);
|
||||
if (!setNamedWidget(create, "fps", opts.outFps || 32) && create.widgets?.[1]) {
|
||||
create.widgets[1].value = opts.outFps || 32; // positional fallback (images, fps, audio)
|
||||
}
|
||||
if (!setNamedWidget(save, "filename_prefix", "video/PromptChain") && save.widgets?.[0]) {
|
||||
save.widgets[0].value = "video/PromptChain";
|
||||
}
|
||||
|
||||
// wiring: VIDEO -> components; images -> RIFE; interpolated -> CreateVideo;
|
||||
// carry the original audio through; CreateVideo -> SaveVideo.
|
||||
try {
|
||||
load.connect(0, comp, 0); // VIDEO -> GetVideoComponents.video
|
||||
comp.connect(0, rife, 0); // images -> RIFE.frames
|
||||
rife.connect(0, create, 0); // interpolated images -> CreateVideo.images
|
||||
comp.connect(1, create, 1); // audio -> CreateVideo.audio (optional)
|
||||
create.connect(0, save, 0); // VIDEO -> SaveVideo.video
|
||||
} catch (e) {
|
||||
console.warn("[Smooth] wiring failed", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return { graph, workflowId };
|
||||
}
|
||||
+23
-4
@@ -507,10 +507,29 @@ function GeneratedGallery($$anchor, $$props) {
|
||||
}
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
if (img.dataset.pcrRetried) return;
|
||||
img.dataset.pcrRetried = "1";
|
||||
img.src = img.src + (img.src.includes("?") ? "&" : "?") + "r=1";
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
function healBrokenThumbs() {
|
||||
if (!galleryEl) return;
|
||||
for (const img of galleryEl.querySelectorAll("img")) {
|
||||
if (img.complete && img.naturalWidth === 0) {
|
||||
img.dataset.pcrRetry = "0";
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", "heal");
|
||||
img.src = u.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
user_effect(() => {
|
||||
const onRecorded = () => healBrokenThumbs();
|
||||
window.addEventListener("promptchain:generation-recorded", onRecorded);
|
||||
return () => window.removeEventListener("promptchain:generation-recorded", onRecorded);
|
||||
});
|
||||
var fragment = root();
|
||||
var div = first_child(fragment);
|
||||
var node = child(div);
|
||||
@@ -782,4 +801,4 @@ export {
|
||||
clampGalleryRowHeight as c,
|
||||
zoomGalleryRowHeight as z
|
||||
};
|
||||
//# sourceMappingURL=GeneratedGallery-Dw0CC3-N.js.map
|
||||
//# sourceMappingURL=GeneratedGallery-D5XbkMk7.js.map
|
||||
@@ -283,7 +283,7 @@ function ModeMenu($$anchor, $$props) {
|
||||
delegate(["click"]);
|
||||
var root_1 = from_html(`<span class="pcr-menubar-mode-arrow svelte-vzwh12"></span>`);
|
||||
var root_2 = from_html(`<span title="AI Assistant (experimental)" data-action="assistant"></span>`);
|
||||
var root_3 = from_html(`<span data-action="pose"></span> <span title="Toggle image preview" data-action="image"></span> <span title="Toggle output panel (Ctrl+\`)" data-action="output"></span>`, 1);
|
||||
var root_3 = from_html(`<span data-action="pose"></span> <span data-action="region"></span> <span title="Toggle image preview" data-action="image"></span> <span title="Toggle output panel (Ctrl+\`)" data-action="output"></span>`, 1);
|
||||
var root_4 = from_html(`<span class="pcr-menubar-btn svelte-vzwh12" title="Fullscreen editor" data-action="maximize"></span> <span title="Collapse editor" data-action="collapse"></span>`, 1);
|
||||
var root = from_html(`<div><div class="pcr-menubar-actions-left"></div> <div style="display:flex;align-items:center;min-width:0;"><div class="pcr-menubar-mode svelte-vzwh12"><span class="pcr-menubar-mode-label svelte-vzwh12"> </span> <!></div> <div class="pcr-menubar-actions svelte-vzwh12"><!> <span title="Lock output" data-action="lock"></span> <span title="Disable node" data-action="disable"></span> <!> <!></div></div></div> <!>`, 1);
|
||||
function Menubar($$anchor, $$props) {
|
||||
@@ -304,6 +304,7 @@ function Menubar($$anchor, $$props) {
|
||||
}), onToggleImage = prop($$props, "onToggleImage", 3, () => {
|
||||
}), onToggleAssistant = prop($$props, "onToggleAssistant", 3, () => {
|
||||
}), onTogglePose = prop($$props, "onTogglePose", 3, () => {
|
||||
}), onToggleRegion = prop($$props, "onToggleRegion", 3, () => {
|
||||
}), onOpenFullscreen = prop($$props, "onOpenFullscreen", 3, () => {
|
||||
}), onResetIterate = prop($$props, "onResetIterate", 3, null), docDropdownEl = prop($$props, "docDropdownEl", 3, null);
|
||||
let docSlot;
|
||||
@@ -393,6 +394,8 @@ function Menubar($$anchor, $$props) {
|
||||
if (get(aiAssistantEnabled)) onToggleAssistant()();
|
||||
} else if (action2 === "pose") {
|
||||
if ($$props.shared.hasPoseStudio) onTogglePose()();
|
||||
} else if (action2 === "region") {
|
||||
if ($$props.shared.hasRegionBox) onToggleRegion()();
|
||||
} else if (action2 === "collapse") onToggleCollapse()();
|
||||
else if (action2 === "maximize") onOpenFullscreen()();
|
||||
}
|
||||
@@ -475,13 +478,18 @@ function Menubar($$anchor, $$props) {
|
||||
var span_6 = sibling(span_5, 2);
|
||||
let classes_5;
|
||||
let styles_6;
|
||||
html(span_6, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>', true);
|
||||
action(span_6, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "image");
|
||||
html(span_6, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5v4h2V5h4V3H5c-1.1 0-2 .9-2 2zm2 10H3v4c0 1.1.9 2 2 2h4v-2H5v-4zm14 4h-4v2h4c1.1 0 2-.9 2-2v-4h-2v4zm0-16h-4v2h4v4h2V5c0-1.1-.9-2-2-2zM7 7v10h10V7H7zm8 8H9V9h6v6z"/></svg>', true);
|
||||
action(span_6, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "region");
|
||||
var span_7 = sibling(span_6, 2);
|
||||
let classes_6;
|
||||
let styles_7;
|
||||
html(span_7, () => '<svg width="18" height="18" viewBox="0 -960 960 960" fill="currentColor"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM80-80v-80h102q-48-23-77.5-68T75-330q0-79 55.5-134.5T265-520v80q-45 0-77.5 32T155-330q0 39 24 69t61 38v-97h80v240H80Zm320-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"/></svg>', true);
|
||||
action(span_7, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "output");
|
||||
html(span_7, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>', true);
|
||||
action(span_7, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "image");
|
||||
var span_8 = sibling(span_7, 2);
|
||||
let classes_7;
|
||||
let styles_8;
|
||||
html(span_8, () => '<svg width="18" height="18" viewBox="0 -960 960 960" fill="currentColor"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM80-80v-80h102q-48-23-77.5-68T75-330q0-79 55.5-134.5T265-520v80q-45 0-77.5 32T155-330q0 39 24 69t61 38v-97h80v240H80Zm320-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"/></svg>', true);
|
||||
action(span_8, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "output");
|
||||
template_effect(() => {
|
||||
classes_4 = set_class(span_5, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_4, {
|
||||
"pcr-menubar-btn-active": $$props.shared.posePanelVisible,
|
||||
@@ -489,10 +497,16 @@ function Menubar($$anchor, $$props) {
|
||||
});
|
||||
set_attribute(span_5, "title", $$props.shared.hasPoseStudio ? "Toggle 3D Poser panel" : "Add a 3D Poser node to use this");
|
||||
styles_5 = set_style(span_5, "", styles_5, { color: $$props.shared.posePanelVisible ? "#ff8c1a" : "" });
|
||||
classes_5 = set_class(span_6, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_5, { "pcr-menubar-btn-active": $$props.shared.imagePanelVisible });
|
||||
styles_6 = set_style(span_6, "", styles_6, { color: $$props.shared.imagePanelVisible ? "#4bb949" : "" });
|
||||
classes_6 = set_class(span_7, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_6, { "pcr-menubar-btn-active": $$props.shared.outputPanelOpen });
|
||||
styles_7 = set_style(span_7, "", styles_7, { color: $$props.shared.outputPanelOpen ? "#4fc3f7" : "" });
|
||||
classes_5 = set_class(span_6, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_5, {
|
||||
"pcr-menubar-btn-active": $$props.shared.regionPanelVisible,
|
||||
"pcr-menubar-btn-disabled": !$$props.shared.hasRegionBox
|
||||
});
|
||||
set_attribute(span_6, "title", $$props.shared.hasRegionBox ? "Toggle Region Box panel" : "Add a Region Box node to use this");
|
||||
styles_6 = set_style(span_6, "", styles_6, { color: $$props.shared.regionPanelVisible ? "#42b9c4" : "" });
|
||||
classes_6 = set_class(span_7, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_6, { "pcr-menubar-btn-active": $$props.shared.imagePanelVisible });
|
||||
styles_7 = set_style(span_7, "", styles_7, { color: $$props.shared.imagePanelVisible ? "#4bb949" : "" });
|
||||
classes_7 = set_class(span_8, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_7, { "pcr-menubar-btn-active": $$props.shared.outputPanelOpen });
|
||||
styles_8 = set_style(span_8, "", styles_8, { color: $$props.shared.outputPanelOpen ? "#4fc3f7" : "" });
|
||||
});
|
||||
append($$anchor2, fragment_1);
|
||||
};
|
||||
@@ -504,17 +518,17 @@ function Menubar($$anchor, $$props) {
|
||||
{
|
||||
var consequent_3 = ($$anchor2) => {
|
||||
var fragment_2 = root_4();
|
||||
var span_8 = first_child(fragment_2);
|
||||
html(span_8, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h7v2H5v5H3V3zm11 0h7v7h-2V5h-5V3zM3 14h2v5h5v2H3v-7zm18 0v7h-7v-2h5v-5h2z"/></svg>', true);
|
||||
action(span_8, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "maximize");
|
||||
var span_9 = sibling(span_8, 2);
|
||||
let classes_7;
|
||||
let styles_8;
|
||||
html(span_9, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6.83 4H20a2 2 0 0 1 2 2v12c0 .34-.09.66-.23.94L20 17.17V8h-9.17l-4-4zm13.66 19.31L17.17 20H4a2 2 0 0 1-2-2V6c0-.34.08-.66.23-.94L.69 3.51L2.1 2.1l19.8 19.8l-1.41 1.41zM15.17 18l-10-10H4v10h11.17z"/></svg>', true);
|
||||
action(span_9, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "collapse");
|
||||
var span_9 = first_child(fragment_2);
|
||||
html(span_9, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h7v2H5v5H3V3zm11 0h7v7h-2V5h-5V3zM3 14h2v5h5v2H3v-7zm18 0v7h-7v-2h5v-5h2z"/></svg>', true);
|
||||
action(span_9, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "maximize");
|
||||
var span_10 = sibling(span_9, 2);
|
||||
let classes_8;
|
||||
let styles_9;
|
||||
html(span_10, () => '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M6.83 4H20a2 2 0 0 1 2 2v12c0 .34-.09.66-.23.94L20 17.17V8h-9.17l-4-4zm13.66 19.31L17.17 20H4a2 2 0 0 1-2-2V6c0-.34.08-.66.23-.94L.69 3.51L2.1 2.1l19.8 19.8l-1.41 1.41zM15.17 18l-10-10H4v10h11.17z"/></svg>', true);
|
||||
action(span_10, ($$node, $$action_arg) => actionClick == null ? void 0 : actionClick($$node, $$action_arg), () => "collapse");
|
||||
template_effect(() => {
|
||||
classes_7 = set_class(span_9, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_7, { "pcr-menubar-btn-active": $$props.shared.collapsed });
|
||||
styles_8 = set_style(span_9, "", styles_8, { color: $$props.shared.collapsed ? "#5e79ff" : "" });
|
||||
classes_8 = set_class(span_10, 1, "pcr-menubar-btn svelte-vzwh12", null, classes_8, { "pcr-menubar-btn-active": $$props.shared.collapsed });
|
||||
styles_9 = set_style(span_10, "", styles_9, { color: $$props.shared.collapsed ? "#5e79ff" : "" });
|
||||
});
|
||||
append($$anchor2, fragment_2);
|
||||
};
|
||||
@@ -592,4 +606,4 @@ export {
|
||||
Menubar as M,
|
||||
SearchableList as S
|
||||
};
|
||||
//# sourceMappingURL=Menubar-DI-z1NzN.js.map
|
||||
//# sourceMappingURL=Menubar-DC12j8gE.js.map
|
||||
@@ -1318,6 +1318,114 @@
|
||||
}
|
||||
.pcr-rp-check.svelte-1elgmyq { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #b6b6be; }
|
||||
|
||||
.pcr-ext-overlay.svelte-5qj8hq {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 10011;
|
||||
}
|
||||
.pcr-ext-modal.svelte-5qj8hq {
|
||||
background: #1e1e1e; border: 1px solid #444; border-radius: 8px;
|
||||
padding: 20px 24px; min-width: 380px; max-width: 480px;
|
||||
max-height: 88vh; overflow-y: auto;
|
||||
}
|
||||
.pcr-ext-title.svelte-5qj8hq { font-size: 15px; font-weight: 600; color: #eee; margin-bottom: 4px; }
|
||||
.pcr-ext-sub.svelte-5qj8hq { font-size: 11px; color: #888; margin-bottom: 14px; line-height: 1.4; }
|
||||
.pcr-ext-frame.svelte-5qj8hq { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.pcr-ext-frame.svelte-5qj8hq img:where(.svelte-5qj8hq) { width: 96px; height: 96px; object-fit: cover; border-radius: 4px; border: 1px solid #333; }
|
||||
.pcr-ext-frame.svelte-5qj8hq span:where(.svelte-5qj8hq) { font-size: 11px; color: #777; }
|
||||
.pcr-ext-label.svelte-5qj8hq { display: block; font-size: 11px; color: #aaa; margin-bottom: 5px; }
|
||||
.pcr-ext-recipe.svelte-5qj8hq { display: flex; gap: 8px; margin-bottom: 14px; }
|
||||
.pcr-ext-recipe-btn.svelte-5qj8hq {
|
||||
flex: 1; display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
|
||||
padding: 8px 10px; background: #161616; border: 1px solid #444; border-radius: 4px;
|
||||
color: #ddd; cursor: pointer; text-align: left; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.pcr-ext-recipe-btn.svelte-5qj8hq:hover:not(:disabled) { border-color: #666; }
|
||||
.pcr-ext-recipe-btn.active.svelte-5qj8hq { border-color: #4fc3f7; background: #15242b; }
|
||||
.pcr-ext-recipe-btn.unavail.svelte-5qj8hq { opacity: 0.5; cursor: not-allowed; }
|
||||
.pcr-ext-recipe-btn.svelte-5qj8hq:disabled { cursor: default; }
|
||||
.pcr-ext-recipe-name.svelte-5qj8hq { font-size: 12px; font-weight: 600; }
|
||||
.pcr-ext-recipe-sub.svelte-5qj8hq { font-size: 10px; color: #888; }
|
||||
.pcr-ext-rec-dot.svelte-5qj8hq { color: #4caf50; font-size: 9px; vertical-align: middle; }
|
||||
.pcr-ext-prompt.svelte-5qj8hq {
|
||||
width: 100%; box-sizing: border-box; min-height: 70px; resize: vertical;
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 8px; font-family: inherit;
|
||||
}
|
||||
.pcr-ext-prompt.svelte-5qj8hq:focus { outline: none; border-color: #4fc3f7; }
|
||||
.pcr-ext-prompt-editor.svelte-5qj8hq { padding: 0; overflow: hidden; min-height: 84px; resize: none; }
|
||||
.pcr-ext-stitch.svelte-5qj8hq { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #aaa; margin-top: 10px; cursor: pointer; }
|
||||
.pcr-ext-adv-toggle.svelte-5qj8hq {
|
||||
margin-top: 12px; background: none; border: none; color: #8bd5f5; font-size: 11px;
|
||||
cursor: pointer; padding: 2px 0;
|
||||
}
|
||||
.pcr-ext-adv.svelte-5qj8hq { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.pcr-ext-row.svelte-5qj8hq { display: flex; flex-direction: column; gap: 4px; }
|
||||
.pcr-ext-row-label.svelte-5qj8hq { font-size: 11px; color: #aaa; display: flex; justify-content: space-between; }
|
||||
.pcr-ext-row-label.svelte-5qj8hq small:where(.svelte-5qj8hq) { color: #6cc; }
|
||||
.pcr-ext-grid2.svelte-5qj8hq { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.pcr-ext-field.svelte-5qj8hq { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #aaa; }
|
||||
.pcr-ext-field.svelte-5qj8hq select:where(.svelte-5qj8hq), .pcr-ext-field.svelte-5qj8hq input[type="number"]:where(.svelte-5qj8hq) {
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 5px 6px; font-family: inherit;
|
||||
}
|
||||
.pcr-ext-field-check.svelte-5qj8hq { flex-direction: row; align-items: center; gap: 6px; align-self: end; padding-bottom: 6px; }
|
||||
.pcr-ext-progress.svelte-5qj8hq { margin-top: 12px; }
|
||||
.pcr-ext-bar.svelte-5qj8hq { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
||||
.pcr-ext-fill.svelte-5qj8hq { height: 100%; background: #4fc3f7; border-radius: 3px; transition: width 0.3s; }
|
||||
.pcr-ext-phase.svelte-5qj8hq { font-size: 11px; color: #aaa; margin-top: 6px; text-transform: capitalize; }
|
||||
.pcr-ext-preview.svelte-5qj8hq, .pcr-ext-result.svelte-5qj8hq { width: 100%; margin-top: 8px; border-radius: 4px; max-height: 240px; }
|
||||
.pcr-ext-err.svelte-5qj8hq { font-size: 12px; color: #f44336; }
|
||||
.pcr-ext-done.svelte-5qj8hq { font-size: 12px; color: #4caf50; margin-bottom: 6px; }
|
||||
.pcr-ext-actions.svelte-5qj8hq { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.pcr-ext-btn.svelte-5qj8hq {
|
||||
padding: 6px 16px; font-size: 12px; border: 1px solid #555; border-radius: 4px;
|
||||
background: transparent; color: #ccc; cursor: pointer;
|
||||
}
|
||||
.pcr-ext-btn.svelte-5qj8hq:hover { background: rgba(255,255,255,0.08); }
|
||||
.pcr-ext-btn.svelte-5qj8hq:disabled { opacity: 0.5; cursor: default; }
|
||||
.pcr-ext-primary.svelte-5qj8hq { background: #4fc3f7; color: #111; border-color: #4fc3f7; }
|
||||
.pcr-ext-primary.svelte-5qj8hq:hover { background: #39b0e4; }
|
||||
.pcr-ext-primary.svelte-5qj8hq:disabled { background: #2a7a9e; }
|
||||
|
||||
.pcr-sm-overlay.svelte-1j60x3s {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 10011;
|
||||
}
|
||||
.pcr-sm-modal.svelte-1j60x3s { background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 20px 24px; min-width: 360px; max-width: 460px; max-height: 88vh; overflow-y: auto; }
|
||||
.pcr-sm-title.svelte-1j60x3s { font-size: 15px; font-weight: 600; color: #eee; margin-bottom: 4px; }
|
||||
.pcr-sm-sub.svelte-1j60x3s { font-size: 11px; color: #888; margin-bottom: 14px; line-height: 1.4; }
|
||||
.pcr-sm-label.svelte-1j60x3s { font-size: 11px; color: #aaa; margin-bottom: 6px; }
|
||||
.pcr-sm-mults.svelte-1j60x3s { display: flex; gap: 8px; }
|
||||
.pcr-sm-mult.svelte-1j60x3s {
|
||||
flex: 1; padding: 8px 0; border: 1px solid #555; border-radius: 4px; background: transparent;
|
||||
color: #ccc; font-size: 13px; cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.pcr-sm-mult.svelte-1j60x3s:hover { border-color: #888; color: #fff; }
|
||||
.pcr-sm-mult.pcr-sm-active.svelte-1j60x3s { border-color: #4fc3f7; color: #4fc3f7; background: rgba(79,195,247,0.1); }
|
||||
.pcr-sm-adv-toggle.svelte-1j60x3s { margin-top: 12px; background: none; border: none; color: #8bd5f5; font-size: 11px; cursor: pointer; padding: 2px 0; }
|
||||
.pcr-sm-adv.svelte-1j60x3s { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.pcr-sm-field.svelte-1j60x3s { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #aaa; }
|
||||
.pcr-sm-field.svelte-1j60x3s small:where(.svelte-1j60x3s), .pcr-sm-check.svelte-1j60x3s small:where(.svelte-1j60x3s) { color: #777; }
|
||||
.pcr-sm-field.svelte-1j60x3s select:where(.svelte-1j60x3s) {
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 5px 6px; font-family: inherit;
|
||||
}
|
||||
.pcr-sm-check.svelte-1j60x3s { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #aaa; cursor: pointer; }
|
||||
.pcr-sm-progress.svelte-1j60x3s { margin-top: 14px; }
|
||||
.pcr-sm-bar.svelte-1j60x3s { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
||||
.pcr-sm-fill.svelte-1j60x3s { height: 100%; background: #4fc3f7; border-radius: 3px; transition: width 0.3s; }
|
||||
.pcr-sm-phase.svelte-1j60x3s { font-size: 11px; color: #aaa; margin-top: 6px; text-transform: capitalize; }
|
||||
.pcr-sm-preview.svelte-1j60x3s, .pcr-sm-result.svelte-1j60x3s { width: 100%; margin-top: 8px; border-radius: 4px; max-height: 240px; }
|
||||
.pcr-sm-err.svelte-1j60x3s { font-size: 12px; color: #f44336; }
|
||||
.pcr-sm-done.svelte-1j60x3s { font-size: 12px; color: #4caf50; margin-bottom: 6px; }
|
||||
.pcr-sm-actions.svelte-1j60x3s { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.pcr-sm-btn.svelte-1j60x3s { padding: 6px 16px; font-size: 12px; border: 1px solid #555; border-radius: 4px; background: transparent; color: #ccc; cursor: pointer; }
|
||||
.pcr-sm-btn.svelte-1j60x3s:hover { background: rgba(255,255,255,0.08); }
|
||||
.pcr-sm-btn.svelte-1j60x3s:disabled { opacity: 0.5; cursor: default; }
|
||||
.pcr-sm-primary.svelte-1j60x3s { background: #4fc3f7; color: #111; border-color: #4fc3f7; }
|
||||
.pcr-sm-primary.svelte-1j60x3s:hover { background: #39b0e4; }
|
||||
.pcr-sm-primary.svelte-1j60x3s:disabled { background: #2a7a9e; }
|
||||
|
||||
.pcr-ctx.svelte-esmc9y {
|
||||
/* Above the viewer overlay (.pcr-viewer is z-index 10001 with an opaque
|
||||
background) — portal moves this menu to <body> as the overlay's sibling,
|
||||
@@ -1424,6 +1532,28 @@
|
||||
}
|
||||
/* centered over the thumb so it never collides with the corner badges (dot,
|
||||
layers, fork pip / expander, opbadge) — the .selected ring already frames it */
|
||||
/* pin marker — top-left; shares the corner with the layers badge, which shifts right when both show */
|
||||
.pcr-lin-pinbadge.svelte-5cciiw {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #ffd24a;
|
||||
box-shadow: 0 0 0 1px rgba(255, 200, 60, 0.6);
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.pcr-viewer-lineage-node.pinned.svelte-5cciiw .pcr-viewer-lineage-layers:where(.svelte-5cciiw) {
|
||||
left: 21px;
|
||||
}
|
||||
.pcr-lin-selcheck.svelte-5cciiw {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -1970,6 +2100,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
.pcr-viewer-history-thumb.svelte-5cciiw {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -2064,6 +2195,7 @@
|
||||
.pcr-viewer-history-grid.svelte-5cciiw::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
|
||||
.pcr-viewer-grid-thumb.svelte-5cciiw {
|
||||
position: relative;
|
||||
width: 56.5px;
|
||||
height: 56.5px;
|
||||
border: 2px solid transparent;
|
||||
@@ -2077,6 +2209,27 @@
|
||||
.pcr-viewer-grid-thumb.svelte-5cciiw:hover:not(.current) { border-color: #666; transform: scale(1.05); z-index: 1; }
|
||||
.pcr-viewer-grid-thumb.svelte-5cciiw img:where(.svelte-5cciiw) { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
|
||||
|
||||
/* ▶ overlay marking a poster-framed thumbnail as a playable video clip */
|
||||
.pcr-viewer-vidbadge.svelte-5cciiw {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding-left: 1px;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* metadata content */
|
||||
.pcr-viewer-meta-content.svelte-5cciiw {
|
||||
flex: 1;
|
||||
@@ -3650,7 +3803,8 @@
|
||||
overflow: hidden;
|
||||
background: var(--pcr-fs-editor-surface, transparent);
|
||||
}
|
||||
.pcr-image-container.svelte-ro1n43 img:where(.svelte-ro1n43) {
|
||||
.pcr-image-container.svelte-ro1n43 img:where(.svelte-ro1n43),
|
||||
.pcr-image-container.svelte-ro1n43 video:where(.svelte-ro1n43) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
@@ -3811,6 +3965,84 @@
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.pcr-region-panel.svelte-wos87s {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
min-width: 240px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.lg-node-widgets .pcr-region-panel.svelte-wos87s {
|
||||
background: rgb(0 0 0 / 46%);
|
||||
}
|
||||
.pcr-region-panel-close-btn.svelte-wos87s {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 0 3px rgba(0, 0, 0, 0.7));
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
.pcr-region-panel-close-btn.pcr-close-dim.svelte-wos87s {
|
||||
opacity: 0.3;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-region-panel-close-btn.pcr-close-show.svelte-wos87s {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-region-panel-body.svelte-wos87s {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--pcr-fs-editor-surface, transparent);
|
||||
}
|
||||
.pcr-region-panel-placeholder.svelte-wos87s {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0 12px;
|
||||
user-select: none;
|
||||
}
|
||||
.pcr-region-divider.svelte-wos87s {
|
||||
width: 4px;
|
||||
background: #242424;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.pcr-region-divider.svelte-wos87s::before {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 1px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.pcr-region-divider.svelte-wos87s:hover::before {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.pcr-footer.svelte-172i5st {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -6607,9 +6839,18 @@
|
||||
color: #5dcaff;
|
||||
min-width: 90px;
|
||||
}
|
||||
/* custom (non-preset) resolution: dim the dropdown, light up the W×H boxes */
|
||||
.pcr-resolution-preset-dimmed.svelte-82atyh {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.pcr-resolution-input-dimmed.svelte-82atyh {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.pcr-resolution-input-active.svelte-82atyh {
|
||||
border-color: #5dcaff;
|
||||
color: #fff;
|
||||
background: #222b33;
|
||||
}
|
||||
.pcr-resolution-preset-menu.svelte-82atyh {
|
||||
position: fixed;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
@@ -7562,6 +7803,14 @@
|
||||
.pcr-precision-installed.svelte-ycwcax:hover {
|
||||
background: rgba(79, 195, 247, 0.25);
|
||||
}
|
||||
.pcr-precision-partial.svelte-ycwcax {
|
||||
background: rgba(255, 183, 77, 0.15);
|
||||
color: #ffb74d;
|
||||
border: 1px solid rgba(255, 183, 77, 0.4);
|
||||
}
|
||||
.pcr-precision-partial.svelte-ycwcax:hover {
|
||||
background: rgba(255, 183, 77, 0.28);
|
||||
}
|
||||
.pcr-precision-missing.svelte-ycwcax {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #888;
|
||||
@@ -7571,6 +7820,14 @@
|
||||
color: #ccc;
|
||||
border-color: #666;
|
||||
}
|
||||
/* a started-but-unfinished download gets an amber edge so it reads as
|
||||
"resume", not "ready" */
|
||||
.pcr-picker-catalog-partial.svelte-ycwcax {
|
||||
border-left-color: #ffb74d;
|
||||
}
|
||||
.pcr-picker-resume-hint.svelte-ycwcax {
|
||||
color: #ffb74d;
|
||||
}
|
||||
|
||||
.pcr-picker-civitai-loading.svelte-ycwcax {
|
||||
padding: 10px 12px;
|
||||
|
||||
+20
-11
@@ -1,15 +1,21 @@
|
||||
const PRIMARY_FOLDERS = /* @__PURE__ */ new Set(["diffusion_models", "unet", "checkpoints"]);
|
||||
function precisionTier(precision) {
|
||||
return /fp16|bf16|fp32/i.test(precision || "") ? "high" : "low";
|
||||
}
|
||||
function extractPrecisions(files) {
|
||||
const precisions = /* @__PURE__ */ new Set();
|
||||
for (const f of files) if (f.variants) for (const v of f.variants) precisions.add(v.precision);
|
||||
return [...precisions];
|
||||
const primary = files.find((f) => f.variants && PRIMARY_FOLDERS.has(f.folder)) || files.find((f) => f.variants);
|
||||
const precisions = [];
|
||||
for (const v of (primary == null ? void 0 : primary.variants) || []) {
|
||||
if (!precisions.includes(v.precision)) precisions.push(v.precision);
|
||||
}
|
||||
return precisions;
|
||||
}
|
||||
function resolveFilesForPrecision(files, precision) {
|
||||
const tier = precisionTier(precision);
|
||||
return files.map((f) => {
|
||||
if (f.variants) {
|
||||
const match = f.variants.find((v) => v.precision === precision);
|
||||
return match ? { label: f.label, folder: f.folder, filename: match.filename, size_bytes: match.size_bytes, source: match.source } : null;
|
||||
}
|
||||
return f;
|
||||
if (!f.variants) return f;
|
||||
const match = f.variants.find((v) => v.precision === precision) || f.variants.find((v) => precisionTier(v.precision) === tier) || f.variants[0];
|
||||
return match ? { label: f.label, folder: f.folder, filename: match.filename, size_bytes: match.size_bytes, source: match.source } : null;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
const ARCHITECTURES = [
|
||||
@@ -66,9 +72,12 @@ const FAMILIES = {
|
||||
{ id: "qwen_aio", label: "Qwen AIO" }
|
||||
],
|
||||
wan22: [
|
||||
{ id: "wan22_t2v", label: "Wan T2V 14B" },
|
||||
{ id: "wan22_i2v", label: "Wan I2V 14B" },
|
||||
{ id: "wan22_5b", label: "Wan 5B" }
|
||||
{ id: "wan22_i2v_gguf", label: "Wan I2V 14B (GGUF)" },
|
||||
{ id: "wan22_t2v", label: "Wan T2V 14B" },
|
||||
{ id: "wan22_t2v_gguf", label: "Wan T2V 14B (GGUF)" },
|
||||
{ id: "wan22_5b", label: "Wan TI2V 5B" },
|
||||
{ id: "wan22_5b_gguf", label: "Wan TI2V 5B (GGUF)" }
|
||||
],
|
||||
ltx: [
|
||||
{ id: "ltx23", label: "LTX 2.3" }
|
||||
@@ -93,4 +102,4 @@ export {
|
||||
extractPrecisions as e,
|
||||
resolveFilesForPrecision as r
|
||||
};
|
||||
//# sourceMappingURL=model-constants-DOP7d958.js.map
|
||||
//# sourceMappingURL=model-constants-cVxhUf51.js.map
|
||||
@@ -9,7 +9,7 @@ import { d as delegate, p as push, a as prop, i as if_block, f as sibling, t as
|
||||
import { a as onMount, o as onDestroy } from "./index-client-iMRCrpBY.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { M as Menubar, S as SearchableList } from "./Menubar-DI-z1NzN.js";
|
||||
import { M as Menubar, S as SearchableList } from "./Menubar-DC12j8gE.js";
|
||||
var root_1$4 = from_html(`<img class="pcr-fs-topbar-logo svelte-93wsqh" alt="PromptChain"/>`);
|
||||
var root$5 = from_html(`<div class="pcr-fs-topbar svelte-93wsqh"><div class="pcr-fs-topbar-left svelte-93wsqh"><!> <span class="pcr-fs-topbar-title svelte-93wsqh"></span></div> <div class="pcr-fs-topbar-right svelte-93wsqh"><div class="pcr-fs-topbar-actions svelte-93wsqh"><input type="number" class="pcr-fs-batch-input svelte-93wsqh" min="1" max="99" title="Batch count"/> <span class="pcr-fs-run-btn svelte-93wsqh" title="Queue prompt (Ctrl+Enter)"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg> <span>Run</span></span> <span class="pcr-fs-cancel-btn pcr-fs-inactive svelte-93wsqh" title="Cancel execution"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg></span> <span class="pcr-fs-queue-badge pcr-fs-inactive svelte-93wsqh">0 active</span></div> <div class="pcr-fs-panel-toggles svelte-93wsqh"><span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle 3D Poser panel" data-fs-action="pose"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle output panel" data-fs-action="output"><svg width="16" height="16" viewBox="0 -960 960 960" fill="currentColor"><path d="M400-280h160v-80H400v80Zm0-160h280v-80H400v80ZM280-600h400v-80H280v80Zm200 120ZM80-80v-80h102q-48-23-77.5-68T75-330q0-79 55.5-134.5T265-520v80q-45 0-77.5 32T155-330q0 39 24 69t61 38v-97h80v240H80Zm320-40v-80h360v-560H200v160h-80v-160q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H400Z"></path></svg></span> <span class="pcr-fs-header-btn svelte-93wsqh" title="Toggle image preview" data-fs-action="image"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"></path></svg></span></div> <span class="pcr-fs-close-btn svelte-93wsqh" title="Close (Escape)"></span></div></div>`);
|
||||
function Topbar($$anchor, $$props) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { G as GeneratedGallery } from "./GeneratedGallery-Dw0CC3-N.js";
|
||||
import { G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
|
||||
function mountGallery(target, props) {
|
||||
return mount(GeneratedGallery, { target, props });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { p as push, a as prop, D as comment, v as first_child, i as if_block, g as get, k as append, l as pop, s as state, e as set, f as sibling, t as template_effect, r as event, A as from_html, n as child, y as set_text, x as set_attribute, d as delegate, c as proxy, u as user_effect, E as untrack, o as bind_this, j as delegated, C as tick, m as user_derived, w as each, z as index, h as set_class, F as update, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { e as extractPrecisions, r as resolveFilesForPrecision } from "./model-constants-DOP7d958.js";
|
||||
import { e as extractPrecisions, r as resolveFilesForPrecision } from "./model-constants-cVxhUf51.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { s as safeJson, H as HttpError } from "./api-context-BFKo1mCD.js";
|
||||
var root_2$4 = from_html(`<div class="pcr-hover-card-version svelte-oulz7g"> </div>`);
|
||||
@@ -104,31 +104,34 @@ var root_1$3 = from_html(`<div class="pcr-model-panel-empty">Loading...</div>`);
|
||||
var root_3$3 = from_html(`<div class="pcr-picker-recognition svelte-ycwcax"> </div>`);
|
||||
var root_5$3 = from_html(`<span class="pcr-model-version-badge svelte-ycwcax"> </span>`);
|
||||
var root_6$3 = from_html(`<span class="pcr-picker-item-meta svelte-ycwcax"> </span>`);
|
||||
var root_4$2 = from_html(`<div><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> <!></span> <!></div> <span class="pcr-picker-item-arrow svelte-ycwcax">▸</span></div>`);
|
||||
var root_8$2 = from_html(`<span> </span>`);
|
||||
var root_7$2 = from_html(`<div class="pcr-picker-item pcr-picker-catalog-item svelte-ycwcax"><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> </span> <div class="pcr-picker-precision-row svelte-ycwcax"></div></div> <span class="pcr-picker-item-arrow svelte-ycwcax">›</span></div>`);
|
||||
var root_11$1 = from_html(`<span class="pcr-model-version-badge svelte-ycwcax"> </span>`);
|
||||
var root_12 = from_html(`<span class="pcr-picker-item-meta svelte-ycwcax"> </span>`);
|
||||
var root_10$2 = from_html(`<div><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> <!></span> <!></div> <span class="pcr-picker-item-arrow svelte-ycwcax"> </span></div>`);
|
||||
var root_9$2 = from_html(`<div class="pcr-picker-civitai-header svelte-ycwcax">AVAILABLE FOR DOWNLOAD</div> <!>`, 1);
|
||||
var root_14 = from_html(`<div class="pcr-picker-item pcr-picker-civitai-item svelte-ycwcax"><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> </span> <span class="pcr-picker-item-meta svelte-ycwcax"> </span></div> <span class="pcr-picker-item-arrow svelte-ycwcax">↓</span></div>`);
|
||||
var root_15 = from_html(`<div class="pcr-picker-civitai-loading svelte-ycwcax">Searching CivitAI…</div>`);
|
||||
var root_16 = from_html(`<div class="pcr-picker-civitai-loading svelte-ycwcax">Loading more…</div>`);
|
||||
var root_17 = from_html(`<div class="pcr-picker-civitai-empty svelte-ycwcax">No results on CivitAI</div>`);
|
||||
var root_13 = from_html(`<div class="pcr-picker-civitai-header svelte-ycwcax">CIVITAI</div> <div class="pcr-picker-civitai-section svelte-ycwcax"><!> <!></div>`, 1);
|
||||
var root_18 = from_html(`<div class="pcr-model-panel-empty">No matches</div>`);
|
||||
var root_7$2 = from_html(`<div class="pcr-picker-precision-row svelte-ycwcax"></div>`);
|
||||
var root_4$2 = from_html(`<div><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> <!></span> <!> <!></div> <span class="pcr-picker-item-arrow svelte-ycwcax">▸</span></div>`);
|
||||
var root_10$2 = from_html(`<span> </span>`);
|
||||
var root_9$2 = from_html(`<div><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> </span> <div class="pcr-picker-precision-row svelte-ycwcax"></div></div> <span class="pcr-picker-item-arrow svelte-ycwcax">›</span></div>`);
|
||||
var root_13 = from_html(`<span class="pcr-model-version-badge svelte-ycwcax"> </span>`);
|
||||
var root_15 = from_html(` <span class="pcr-picker-resume-hint svelte-ycwcax">resume ↻</span>`, 1);
|
||||
var root_14 = from_html(`<span class="pcr-picker-item-meta svelte-ycwcax"> <!></span>`);
|
||||
var root_12 = from_html(`<div><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> <!></span> <!></div> <span class="pcr-picker-item-arrow svelte-ycwcax"> </span></div>`);
|
||||
var root_11$1 = from_html(`<div class="pcr-picker-civitai-header svelte-ycwcax">AVAILABLE FOR DOWNLOAD</div> <!>`, 1);
|
||||
var root_17 = from_html(`<div class="pcr-picker-item pcr-picker-civitai-item svelte-ycwcax"><div class="pcr-picker-item-info svelte-ycwcax"><span class="pcr-picker-item-name svelte-ycwcax"> </span> <span class="pcr-picker-item-meta svelte-ycwcax"> </span></div> <span class="pcr-picker-item-arrow svelte-ycwcax">↓</span></div>`);
|
||||
var root_18 = from_html(`<div class="pcr-picker-civitai-loading svelte-ycwcax">Searching CivitAI…</div>`);
|
||||
var root_19 = from_html(`<div class="pcr-picker-civitai-loading svelte-ycwcax">Loading more…</div>`);
|
||||
var root_20 = from_html(`<div class="pcr-picker-civitai-empty svelte-ycwcax">No results on CivitAI</div>`);
|
||||
var root_16 = from_html(`<div class="pcr-picker-civitai-header svelte-ycwcax">CIVITAI</div> <div class="pcr-picker-civitai-section svelte-ycwcax"><!> <!></div>`, 1);
|
||||
var root_21 = from_html(`<div class="pcr-model-panel-empty">No matches</div>`);
|
||||
var root_2$3 = from_html(`<div class="pcr-picker-header svelte-ycwcax"><input class="pcr-picker-search svelte-ycwcax" type="text" placeholder="Search models..."/> <!></div> <div><!> <!> <!> <!> <!></div>`, 1);
|
||||
var root_20 = from_html(`<div><span> </span> <span class="pcr-picker-version-arrow svelte-ycwcax">▶</span></div>`);
|
||||
var root_19 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax">SELECT VERSION</div> <!></div>`);
|
||||
var root_22 = from_html(`<div class="pcr-picker-version-item svelte-ycwcax"><span> </span> <span class="pcr-picker-version-arrow svelte-ycwcax">↓</span></div>`);
|
||||
var root_21 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax">SELECT VERSION</div> <!></div>`);
|
||||
var root_25 = from_html(`<div class="pcr-picker-category-header svelte-ycwcax">TEMPLATES</div>`);
|
||||
var root_24 = from_html(`<div class="pcr-picker-submenu-item pcr-picker-current-workflow svelte-ycwcax" title="Swap this checkpoint into your current graph and reapply its settings — keeps your wiring (regional couple, etc.)">(Current Workflow)</div> <!>`, 1);
|
||||
var root_29 = from_html(`<div class="pcr-picker-category-header svelte-ycwcax"> </div>`);
|
||||
var root_30 = from_html(`<div class="pcr-picker-submenu-item svelte-ycwcax"> </div>`);
|
||||
var root_28 = from_html(`<!> <!>`, 1);
|
||||
var root_31 = from_html(`<div class="pcr-picker-submenu-item pcr-picker-submenu-hint svelte-ycwcax">No templates available</div>`);
|
||||
var root_23 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax"> </div> <!> <!></div>`);
|
||||
var root_23 = from_html(`<div><span> </span> <span class="pcr-picker-version-arrow svelte-ycwcax">▶</span></div>`);
|
||||
var root_22 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax">SELECT VERSION</div> <!></div>`);
|
||||
var root_25 = from_html(`<div class="pcr-picker-version-item svelte-ycwcax"><span> </span> <span class="pcr-picker-version-arrow svelte-ycwcax">↓</span></div>`);
|
||||
var root_24 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax">SELECT VERSION</div> <!></div>`);
|
||||
var root_28 = from_html(`<div class="pcr-picker-category-header svelte-ycwcax">TEMPLATES</div>`);
|
||||
var root_27 = from_html(`<div class="pcr-picker-submenu-item pcr-picker-current-workflow svelte-ycwcax" title="Swap this checkpoint into your current graph and reapply its settings — keeps your wiring (regional couple, etc.)">(Current Workflow)</div> <!>`, 1);
|
||||
var root_32 = from_html(`<div class="pcr-picker-category-header svelte-ycwcax"> </div>`);
|
||||
var root_33 = from_html(`<div class="pcr-picker-submenu-item svelte-ycwcax"> </div>`);
|
||||
var root_31 = from_html(`<!> <!>`, 1);
|
||||
var root_34 = from_html(`<div class="pcr-picker-submenu-item pcr-picker-submenu-hint svelte-ycwcax">No templates available</div>`);
|
||||
var root_26 = from_html(`<div class="pcr-picker-submenu svelte-ycwcax"><div class="pcr-picker-submenu-header svelte-ycwcax"> </div> <!> <!></div>`);
|
||||
var root$3 = from_html(`<div class="pcr-model-picker svelte-ycwcax"><!></div> <!> <!> <!> <!>`, 1);
|
||||
function ModelPicker($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
@@ -177,7 +180,11 @@ function ModelPicker($$anchor, $$props) {
|
||||
let listEl;
|
||||
let searchEl;
|
||||
let panelStyle = state("");
|
||||
let catalogPrecisionStatus = state(proxy({}));
|
||||
let catalogPrecisionStatus = user_derived(() => {
|
||||
const out = {};
|
||||
for (const c of get(catalog)) out[c.hash] = c.precision_status || {};
|
||||
return out;
|
||||
});
|
||||
async function loadData() {
|
||||
try {
|
||||
const [modelsRes, templatesRes, catalogRes] = await Promise.all([
|
||||
@@ -198,7 +205,6 @@ function ModelPicker($$anchor, $$props) {
|
||||
}).then((r) => r.json());
|
||||
set(modelSettings, settingsRes.settings || {}, true);
|
||||
}
|
||||
await checkCatalogPrecisions();
|
||||
set(loading, false);
|
||||
requestAnimationFrame(() => searchEl == null ? void 0 : searchEl.focus());
|
||||
focusDownloadedModel();
|
||||
@@ -234,43 +240,6 @@ function ModelPicker($$anchor, $$props) {
|
||||
await tick();
|
||||
(_b = listEl == null ? void 0 : listEl.querySelector(".pcr-picker-item-highlight")) == null ? void 0 : _b.scrollIntoView({ block: "center" });
|
||||
}
|
||||
async function checkCatalogPrecisions() {
|
||||
if (!get(catalog).length) return;
|
||||
const filesToCheck = [];
|
||||
const fileMap = [];
|
||||
const status = {};
|
||||
for (const entry of get(catalog)) {
|
||||
status[entry.hash] = {};
|
||||
for (const f of entry.files || []) {
|
||||
if (!f.variants) continue;
|
||||
for (const v of f.variants) {
|
||||
if (f.folder === "diffusion_models" || f.folder === "unet") {
|
||||
filesToCheck.push({ filename: v.filename, folder: f.folder });
|
||||
fileMap.push({ entryHash: entry.hash, precision: v.precision });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!filesToCheck.length) {
|
||||
set(catalogPrecisionStatus, status, true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const checkRes = await fetch("/promptchain/models/check-files", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ files: filesToCheck })
|
||||
});
|
||||
const checkData = await checkRes.json();
|
||||
for (let i = 0; i < (checkData.results || []).length; i++) {
|
||||
if (checkData.results[i].exists) {
|
||||
status[fileMap[i].entryHash][fileMap[i].precision] = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
set(catalogPrecisionStatus, status, true);
|
||||
}
|
||||
let groupedModels = user_derived(() => {
|
||||
const sorted = get(models).slice().sort((a, b) => {
|
||||
var _a, _b;
|
||||
@@ -327,7 +296,7 @@ function ModelPicker($$anchor, $$props) {
|
||||
const terms = q ? q.split(/\s+/) : [];
|
||||
return get(catalog).filter((c) => {
|
||||
const status = get(catalogPrecisionStatus)[c.hash] || {};
|
||||
if (!Object.values(status).some((v) => v)) return false;
|
||||
if (!Object.values(status).some((v) => v === "installed" || v === "partial")) return false;
|
||||
if (!terms.length) return true;
|
||||
const text = [
|
||||
c.display_name,
|
||||
@@ -343,7 +312,7 @@ function ModelPicker($$anchor, $$props) {
|
||||
const q = get(searchQuery).toLowerCase().trim();
|
||||
let items = get(catalog).filter((c) => {
|
||||
const status = get(catalogPrecisionStatus)[c.hash] || {};
|
||||
return !Object.values(status).some((v) => v);
|
||||
return !Object.values(status).some((v) => v === "installed" || v === "partial");
|
||||
});
|
||||
if (q) {
|
||||
const terms = q.split(/\s+/);
|
||||
@@ -363,10 +332,13 @@ function ModelPicker($$anchor, $$props) {
|
||||
name: entry.model_name || entry.display_name || "",
|
||||
architecture: entry.architecture || "",
|
||||
family: entry.family || "",
|
||||
partial: false,
|
||||
versions: []
|
||||
});
|
||||
}
|
||||
groups.get(key).versions.push(entry);
|
||||
const g = groups.get(key);
|
||||
g.versions.push(entry);
|
||||
if (entry.partial) g.partial = true;
|
||||
}
|
||||
for (const g of groups.values()) {
|
||||
g.versions.sort((a, b) => (b.version || "").localeCompare(a.version || "", void 0, { numeric: true }));
|
||||
@@ -484,16 +456,15 @@ function ModelPicker($$anchor, $$props) {
|
||||
return scope.architecture === arch;
|
||||
});
|
||||
}
|
||||
async function handleCatalogPrecisionClick(entry, prec, installed) {
|
||||
async function handleCatalogPrecisionClick(entry, prec, state2) {
|
||||
var _a;
|
||||
if (!installed) {
|
||||
$$props.onShowCatalogDownload(entry);
|
||||
if (state2 !== "installed") {
|
||||
$$props.onShowCatalogDownload(entry, prec);
|
||||
return;
|
||||
}
|
||||
const resolved = resolveFilesForPrecision(entry.files, prec);
|
||||
const diffFile = resolved.find((f) => f.folder === "diffusion_models" || f.folder === "unet");
|
||||
if (!diffFile) return;
|
||||
const localModel = get(models).find((m) => m.filename === diffFile.filename);
|
||||
const localModel = diffFile && get(models).find((m) => m.filename === diffFile.filename);
|
||||
if (localModel && templatesForModel(localModel).length > 0) {
|
||||
set(
|
||||
activeSubmenu,
|
||||
@@ -505,10 +476,68 @@ function ModelPicker($$anchor, $$props) {
|
||||
true
|
||||
);
|
||||
set(activeNestedSubmenu, null);
|
||||
} else {
|
||||
$$props.onShowCatalogDownload(entry, prec);
|
||||
}
|
||||
}
|
||||
function primaryFileEntry(settings) {
|
||||
const files = (settings == null ? void 0 : settings.files) || [];
|
||||
const isPrimaryFolder = (f) => f.folder === "diffusion_models" || f.folder === "unet" || f.folder === "checkpoints";
|
||||
return files.find((f) => f.primary && isPrimaryFolder(f)) || files.find(isPrimaryFolder) || null;
|
||||
}
|
||||
function groupPrecisions(group) {
|
||||
var _a, _b;
|
||||
const settings = get(modelSettings)[(_a = group.versions[0]) == null ? void 0 : _a.hash];
|
||||
const variants = ((_b = primaryFileEntry(settings)) == null ? void 0 : _b.variants) || [];
|
||||
if (variants.length < 2) return [];
|
||||
return variants.map((v) => {
|
||||
const fnameLower = (v.filename || "").toLowerCase();
|
||||
const localModel = get(models).find((m) => (m.filename || "").toLowerCase() === fnameLower);
|
||||
return {
|
||||
precision: v.precision,
|
||||
filename: v.filename,
|
||||
installed: !!localModel,
|
||||
model: localModel || null
|
||||
};
|
||||
});
|
||||
}
|
||||
function entryForGroup(group) {
|
||||
var _a, _b;
|
||||
const s = get(modelSettings)[(_a = group.versions[0]) == null ? void 0 : _a.hash] || {};
|
||||
const entry = {
|
||||
hash: (_b = group.versions[0]) == null ? void 0 : _b.hash,
|
||||
display_name: s.display_name || group.name,
|
||||
model_name: s.model_name || group.name,
|
||||
version: s.version || "",
|
||||
architecture: s.architecture || group.architecture || "",
|
||||
family: s.family || group.family || "",
|
||||
description: s.description || "",
|
||||
files: s.files || []
|
||||
};
|
||||
if (s.civitai_model_id) entry.civitai_model_id = s.civitai_model_id;
|
||||
if (s.thumbnail) entry.thumbnail = s.thumbnail;
|
||||
return entry;
|
||||
}
|
||||
function handleInstalledPrecisionClick(prec) {
|
||||
var _a;
|
||||
if (prec.model && templatesForModel(prec.model).length > 0) {
|
||||
set(activeNestedSubmenu, null);
|
||||
set(
|
||||
activeSubmenu,
|
||||
{
|
||||
type: "template",
|
||||
model: prec.model,
|
||||
displayName: ((_a = get(modelSettings)[prec.model.hash]) == null ? void 0 : _a.display_name) || prec.model.filename
|
||||
},
|
||||
true
|
||||
);
|
||||
} else {
|
||||
$$props.onClose();
|
||||
}
|
||||
}
|
||||
function handleMissingPrecisionClick(group, prec) {
|
||||
$$props.onShowCatalogDownload(entryForGroup(group), prec.precision);
|
||||
}
|
||||
function sortCategories(cats) {
|
||||
const known = (get(categoryOrder) || []).filter((c) => cats.includes(c));
|
||||
const unknown = cats.filter((c) => {
|
||||
@@ -522,6 +551,26 @@ function ModelPicker($$anchor, $$props) {
|
||||
if (settings == null ? void 0 : settings.version_label) return settings.version_label;
|
||||
const version = (settings == null ? void 0 : settings.version) || "";
|
||||
const filename = (model.filename || "").toLowerCase();
|
||||
const precision = declaredPrecision(settings, filename) || sniffPrecision(filename);
|
||||
if (precision && version) {
|
||||
const esc = precision.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (version.toLowerCase().includes(precision.toLowerCase())) {
|
||||
return version.replace(new RegExp(`\\s*${esc}`, "i"), ` (${precision})`);
|
||||
}
|
||||
return `${version} (${precision})`;
|
||||
}
|
||||
return version || model.filename || "";
|
||||
}
|
||||
function declaredPrecision(settings, filenameLower) {
|
||||
for (const f of (settings == null ? void 0 : settings.files) || []) {
|
||||
if ((f.filename || "").toLowerCase() === filenameLower) return f.precision || null;
|
||||
for (const v of f.variants || []) {
|
||||
if ((v.filename || "").toLowerCase() === filenameLower) return v.precision || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function sniffPrecision(filenameLower) {
|
||||
const precisionPatterns = [
|
||||
[/bf16/i, "BF16"],
|
||||
[/fp16/i, "FP16"],
|
||||
@@ -531,20 +580,10 @@ function ModelPicker($$anchor, $$props) {
|
||||
[/fp4/i, "FP4"],
|
||||
[/\.gguf$/i, "GGUF"]
|
||||
];
|
||||
let precision = null;
|
||||
for (const [pattern, label] of precisionPatterns) {
|
||||
if (pattern.test(filename)) {
|
||||
precision = label;
|
||||
break;
|
||||
}
|
||||
if (pattern.test(filenameLower)) return label;
|
||||
}
|
||||
if (precision && version) {
|
||||
if (version.toLowerCase().includes(precision.toLowerCase())) {
|
||||
return version.replace(new RegExp(`\\s*${precision}`, "i"), ` (${precision})`);
|
||||
}
|
||||
return `${version} (${precision})`;
|
||||
}
|
||||
return version || model.filename || "";
|
||||
return null;
|
||||
}
|
||||
function buildCatalogVersionLabel(entry) {
|
||||
var _a, _b;
|
||||
@@ -724,6 +763,7 @@ function ModelPicker($$anchor, $$props) {
|
||||
const model = user_derived(() => get(group).versions[0]);
|
||||
const displayName = user_derived(() => get(group).name);
|
||||
const meta = user_derived(() => [get(group).architecture, get(group).family].filter(Boolean).join(" · "));
|
||||
const precs = user_derived(() => groupPrecisions(get(group)));
|
||||
var div_5 = root_4$2();
|
||||
let classes_1;
|
||||
var div_6 = child(div_5);
|
||||
@@ -738,7 +778,7 @@ function ModelPicker($$anchor, $$props) {
|
||||
append($$anchor4, span_1);
|
||||
};
|
||||
if_block(node_3, ($$render) => {
|
||||
if (get(group).versions.length > 1) $$render(consequent_2);
|
||||
if (get(group).versions.length > 1 && !get(precs).length) $$render(consequent_2);
|
||||
});
|
||||
}
|
||||
var node_4 = sibling(span, 2);
|
||||
@@ -753,6 +793,40 @@ function ModelPicker($$anchor, $$props) {
|
||||
if (get(meta)) $$render(consequent_3);
|
||||
});
|
||||
}
|
||||
var node_5 = sibling(node_4, 2);
|
||||
{
|
||||
var consequent_4 = ($$anchor4) => {
|
||||
var div_7 = root_7$2();
|
||||
each(div_7, 21, () => get(precs), index, ($$anchor5, p) => {
|
||||
var span_3 = root_8$2();
|
||||
let classes_2;
|
||||
var text_5 = child(span_3);
|
||||
template_effect(
|
||||
($0, $1) => {
|
||||
classes_2 = set_class(span_3, 1, "pcr-picker-precision-tag svelte-ycwcax", null, classes_2, {
|
||||
"pcr-precision-installed": get(p).installed,
|
||||
"pcr-precision-missing": !get(p).installed
|
||||
});
|
||||
set_attribute(span_3, "title", $0);
|
||||
set_text(text_5, `${$1 ?? ""}${get(p).installed ? "" : " ↓"}`);
|
||||
},
|
||||
[
|
||||
() => get(p).installed ? `${get(p).precision.toUpperCase()} installed — use it` : `Download ${get(p).precision.toUpperCase()}`,
|
||||
() => get(p).precision.toUpperCase()
|
||||
]
|
||||
);
|
||||
delegated("click", span_3, (e) => {
|
||||
e.stopPropagation();
|
||||
get(p).installed ? handleInstalledPrecisionClick(get(p)) : handleMissingPrecisionClick(get(group), get(p));
|
||||
});
|
||||
append($$anchor5, span_3);
|
||||
});
|
||||
append($$anchor4, div_7);
|
||||
};
|
||||
if_block(node_5, ($$render) => {
|
||||
if (get(precs).length) $$render(consequent_4);
|
||||
});
|
||||
}
|
||||
template_effect(() => {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
classes_1 = set_class(div_5, 1, "pcr-picker-item svelte-ycwcax", null, classes_1, {
|
||||
@@ -775,123 +849,148 @@ function ModelPicker($$anchor, $$props) {
|
||||
event("pointerleave", div_5, onItemLeave);
|
||||
append($$anchor3, div_5);
|
||||
});
|
||||
var node_5 = sibling(node_2, 2);
|
||||
each(node_5, 17, () => get(catalogInstalled), index, ($$anchor3, entry) => {
|
||||
var node_6 = sibling(node_2, 2);
|
||||
each(node_6, 17, () => get(catalogInstalled), index, ($$anchor3, entry) => {
|
||||
const precisions = user_derived(() => extractPrecisions(get(entry).files || []));
|
||||
const status = user_derived(() => get(catalogPrecisionStatus)[get(entry).hash] || {});
|
||||
var div_7 = root_7$2();
|
||||
var div_8 = child(div_7);
|
||||
var span_3 = child(div_8);
|
||||
var text_5 = child(span_3);
|
||||
var div_9 = sibling(span_3, 2);
|
||||
each(div_9, 21, () => get(precisions), index, ($$anchor4, prec) => {
|
||||
const installed = user_derived(() => !!get(status)[get(prec)]);
|
||||
var span_4 = root_8$2();
|
||||
let classes_2;
|
||||
var text_6 = child(span_4);
|
||||
var div_8 = root_9$2();
|
||||
let classes_3;
|
||||
var div_9 = child(div_8);
|
||||
var span_4 = child(div_9);
|
||||
var text_6 = child(span_4);
|
||||
var div_10 = sibling(span_4, 2);
|
||||
each(div_10, 21, () => get(precisions), index, ($$anchor4, prec) => {
|
||||
const st = user_derived(() => get(status)[get(prec)] || "missing");
|
||||
var span_5 = root_10$2();
|
||||
let classes_4;
|
||||
var text_7 = child(span_5);
|
||||
template_effect(
|
||||
($0, $1) => {
|
||||
classes_2 = set_class(span_4, 1, "pcr-picker-precision-tag svelte-ycwcax", null, classes_2, {
|
||||
"pcr-precision-installed": get(installed),
|
||||
"pcr-precision-missing": !get(installed)
|
||||
classes_4 = set_class(span_5, 1, "pcr-picker-precision-tag svelte-ycwcax", null, classes_4, {
|
||||
"pcr-precision-installed": get(st) === "installed",
|
||||
"pcr-precision-partial": get(st) === "partial",
|
||||
"pcr-precision-missing": get(st) === "missing"
|
||||
});
|
||||
set_attribute(span_4, "title", $0);
|
||||
set_text(text_6, `${$1 ?? ""}${get(installed) ? "" : " ↓"}`);
|
||||
set_attribute(span_5, "title", $0);
|
||||
set_text(text_7, `${$1 ?? ""}${get(st) === "installed" ? "" : get(st) === "partial" ? " ↻" : " ↓"}`);
|
||||
},
|
||||
[
|
||||
() => get(installed) ? `${get(prec).toUpperCase()} installed` : `Download ${get(prec).toUpperCase()}`,
|
||||
() => get(st) === "installed" ? `${get(prec).toUpperCase()} installed` : get(st) === "partial" ? `${get(prec).toUpperCase()} download unfinished — click to resume` : `Download ${get(prec).toUpperCase()}`,
|
||||
() => get(prec).toUpperCase()
|
||||
]
|
||||
);
|
||||
delegated("click", span_4, (e) => {
|
||||
delegated("click", span_5, (e) => {
|
||||
e.stopPropagation();
|
||||
handleCatalogPrecisionClick(get(entry), get(prec), get(installed));
|
||||
handleCatalogPrecisionClick(get(entry), get(prec), get(st));
|
||||
});
|
||||
append($$anchor4, span_4);
|
||||
append($$anchor4, span_5);
|
||||
});
|
||||
template_effect(() => set_text(text_5, get(entry).display_name || get(entry).model_name));
|
||||
delegated("click", div_7, (e) => {
|
||||
template_effect(
|
||||
($0) => {
|
||||
classes_3 = set_class(div_8, 1, "pcr-picker-item pcr-picker-catalog-item svelte-ycwcax", null, classes_3, $0);
|
||||
set_text(text_6, get(entry).display_name || get(entry).model_name);
|
||||
},
|
||||
[
|
||||
() => ({
|
||||
"pcr-picker-catalog-partial": get(precisions).some((p) => get(status)[p] === "partial")
|
||||
})
|
||||
]
|
||||
);
|
||||
delegated("click", div_8, (e) => {
|
||||
e.stopPropagation();
|
||||
const installedPrec = get(precisions).find((p) => get(status)[p]);
|
||||
if (installedPrec) handleCatalogPrecisionClick(get(entry), installedPrec, true);
|
||||
const prec = get(precisions).find((p) => get(status)[p] === "installed") || get(precisions).find((p) => get(status)[p] === "partial");
|
||||
if (prec) handleCatalogPrecisionClick(get(entry), prec, get(status)[prec]);
|
||||
});
|
||||
append($$anchor3, div_7);
|
||||
append($$anchor3, div_8);
|
||||
});
|
||||
var node_6 = sibling(node_5, 2);
|
||||
var node_7 = sibling(node_6, 2);
|
||||
{
|
||||
var consequent_6 = ($$anchor3) => {
|
||||
var fragment_2 = root_9$2();
|
||||
var node_7 = sibling(first_child(fragment_2), 2);
|
||||
each(node_7, 17, () => get(catalogGroups), (group) => group.name, ($$anchor4, group) => {
|
||||
var consequent_8 = ($$anchor3) => {
|
||||
var fragment_2 = root_11$1();
|
||||
var node_8 = sibling(first_child(fragment_2), 2);
|
||||
each(node_8, 17, () => get(catalogGroups), (group) => group.name, ($$anchor4, group) => {
|
||||
const meta = user_derived(() => [get(group).architecture, get(group).family].filter(Boolean).join(" · "));
|
||||
var div_10 = root_10$2();
|
||||
let classes_3;
|
||||
var div_11 = child(div_10);
|
||||
var span_5 = child(div_11);
|
||||
var text_7 = child(span_5);
|
||||
var node_8 = sibling(text_7);
|
||||
{
|
||||
var consequent_4 = ($$anchor5) => {
|
||||
var span_6 = root_11$1();
|
||||
var text_8 = child(span_6);
|
||||
template_effect(() => set_text(text_8, get(group).versions.length));
|
||||
append($$anchor5, span_6);
|
||||
};
|
||||
if_block(node_8, ($$render) => {
|
||||
if (get(group).versions.length > 1) $$render(consequent_4);
|
||||
});
|
||||
}
|
||||
var node_9 = sibling(span_5, 2);
|
||||
var div_11 = root_12();
|
||||
let classes_5;
|
||||
var div_12 = child(div_11);
|
||||
var span_6 = child(div_12);
|
||||
var text_8 = child(span_6);
|
||||
var node_9 = sibling(text_8);
|
||||
{
|
||||
var consequent_5 = ($$anchor5) => {
|
||||
var span_7 = root_12();
|
||||
var span_7 = root_13();
|
||||
var text_9 = child(span_7);
|
||||
template_effect(() => set_text(text_9, get(meta)));
|
||||
template_effect(() => set_text(text_9, get(group).versions.length));
|
||||
append($$anchor5, span_7);
|
||||
};
|
||||
if_block(node_9, ($$render) => {
|
||||
if (get(meta)) $$render(consequent_5);
|
||||
if (get(group).versions.length > 1) $$render(consequent_5);
|
||||
});
|
||||
}
|
||||
var span_8 = sibling(div_11, 2);
|
||||
var text_10 = child(span_8);
|
||||
var node_10 = sibling(span_6, 2);
|
||||
{
|
||||
var consequent_7 = ($$anchor5) => {
|
||||
var span_8 = root_14();
|
||||
var text_10 = child(span_8);
|
||||
var node_11 = sibling(text_10);
|
||||
{
|
||||
var consequent_6 = ($$anchor6) => {
|
||||
var fragment_3 = root_15();
|
||||
var text_11 = first_child(fragment_3, true);
|
||||
template_effect(() => set_text(text_11, get(meta) ? " · " : ""));
|
||||
append($$anchor6, fragment_3);
|
||||
};
|
||||
if_block(node_11, ($$render) => {
|
||||
if (get(group).partial) $$render(consequent_6);
|
||||
});
|
||||
}
|
||||
template_effect(() => set_text(text_10, get(meta)));
|
||||
append($$anchor5, span_8);
|
||||
};
|
||||
if_block(node_10, ($$render) => {
|
||||
if (get(meta) || get(group).partial) $$render(consequent_7);
|
||||
});
|
||||
}
|
||||
var span_9 = sibling(div_12, 2);
|
||||
var text_12 = child(span_9);
|
||||
template_effect(() => {
|
||||
var _a, _b;
|
||||
classes_3 = set_class(div_10, 1, "pcr-picker-item pcr-picker-catalog-item svelte-ycwcax", null, classes_3, {
|
||||
"pcr-picker-item-active": ((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "catalogVersion" && ((_b = get(activeSubmenu)) == null ? void 0 : _b.group) === get(group)
|
||||
classes_5 = set_class(div_11, 1, "pcr-picker-item pcr-picker-catalog-item svelte-ycwcax", null, classes_5, {
|
||||
"pcr-picker-item-active": ((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "catalogVersion" && ((_b = get(activeSubmenu)) == null ? void 0 : _b.group) === get(group),
|
||||
"pcr-picker-catalog-partial": get(group).partial
|
||||
});
|
||||
set_text(text_7, `${get(group).name ?? ""} `);
|
||||
set_text(text_10, get(group).versions.length > 1 ? "▸" : "↓");
|
||||
set_text(text_8, `${get(group).name ?? ""} `);
|
||||
set_text(text_12, get(group).versions.length > 1 ? "▸" : get(group).partial ? "↻" : "↓");
|
||||
});
|
||||
delegated("click", div_10, (e) => {
|
||||
delegated("click", div_11, (e) => {
|
||||
e.stopPropagation();
|
||||
handleCatalogGroupClick(get(group));
|
||||
});
|
||||
append($$anchor4, div_10);
|
||||
append($$anchor4, div_11);
|
||||
});
|
||||
append($$anchor3, fragment_2);
|
||||
};
|
||||
if_block(node_6, ($$render) => {
|
||||
if (get(catalogGroups).length) $$render(consequent_6);
|
||||
if_block(node_7, ($$render) => {
|
||||
if (get(catalogGroups).length) $$render(consequent_8);
|
||||
});
|
||||
}
|
||||
var node_10 = sibling(node_6, 2);
|
||||
var node_12 = sibling(node_7, 2);
|
||||
{
|
||||
var consequent_10 = ($$anchor3) => {
|
||||
var fragment_3 = root_13();
|
||||
var div_12 = sibling(first_child(fragment_3), 2);
|
||||
var node_11 = child(div_12);
|
||||
each(node_11, 17, () => get(civitaiResults), index, ($$anchor4, result) => {
|
||||
var div_13 = root_14();
|
||||
var div_14 = child(div_13);
|
||||
var span_9 = child(div_14);
|
||||
var text_11 = child(span_9);
|
||||
var span_10 = sibling(span_9, 2);
|
||||
var text_12 = child(span_10);
|
||||
var consequent_12 = ($$anchor3) => {
|
||||
var fragment_4 = root_16();
|
||||
var div_13 = sibling(first_child(fragment_4), 2);
|
||||
var node_13 = child(div_13);
|
||||
each(node_13, 17, () => get(civitaiResults), index, ($$anchor4, result) => {
|
||||
var div_14 = root_17();
|
||||
var div_15 = child(div_14);
|
||||
var span_10 = child(div_15);
|
||||
var text_13 = child(span_10);
|
||||
var span_11 = sibling(span_10, 2);
|
||||
var text_14 = child(span_11);
|
||||
template_effect(
|
||||
($0) => {
|
||||
set_text(text_11, get(result).model_name);
|
||||
set_text(text_12, $0);
|
||||
set_text(text_13, get(result).model_name);
|
||||
set_text(text_14, $0);
|
||||
},
|
||||
[
|
||||
() => {
|
||||
@@ -905,8 +1004,8 @@ function ModelPicker($$anchor, $$props) {
|
||||
}
|
||||
]
|
||||
);
|
||||
delegated("click", div_13, () => $$props.onShowDownload(get(result)));
|
||||
event("pointerenter", div_13, (e) => onItemEnter(e, {
|
||||
delegated("click", div_14, () => $$props.onShowDownload(get(result)));
|
||||
event("pointerenter", div_14, (e) => onItemEnter(e, {
|
||||
thumbnail: get(result).thumbnail,
|
||||
name: get(result).model_name,
|
||||
version: get(result).version,
|
||||
@@ -915,44 +1014,44 @@ function ModelPicker($$anchor, $$props) {
|
||||
downloads: get(result).downloads,
|
||||
tags: get(result).tags
|
||||
}));
|
||||
event("pointerleave", div_13, onItemLeave);
|
||||
append($$anchor4, div_13);
|
||||
event("pointerleave", div_14, onItemLeave);
|
||||
append($$anchor4, div_14);
|
||||
});
|
||||
var node_12 = sibling(node_11, 2);
|
||||
var node_14 = sibling(node_13, 2);
|
||||
{
|
||||
var consequent_7 = ($$anchor4) => {
|
||||
var div_15 = root_15();
|
||||
append($$anchor4, div_15);
|
||||
};
|
||||
var consequent_8 = ($$anchor4) => {
|
||||
var div_16 = root_16();
|
||||
var consequent_9 = ($$anchor4) => {
|
||||
var div_16 = root_18();
|
||||
append($$anchor4, div_16);
|
||||
};
|
||||
var consequent_9 = ($$anchor4) => {
|
||||
var div_17 = root_17();
|
||||
var consequent_10 = ($$anchor4) => {
|
||||
var div_17 = root_19();
|
||||
append($$anchor4, div_17);
|
||||
};
|
||||
if_block(node_12, ($$render) => {
|
||||
if (get(civitaiLoading) && !get(civitaiResults).length) $$render(consequent_7);
|
||||
else if (get(civitaiLoading)) $$render(consequent_8, 1);
|
||||
else if (get(civitaiHasSearched) && !get(civitaiResults).length) $$render(consequent_9, 2);
|
||||
var consequent_11 = ($$anchor4) => {
|
||||
var div_18 = root_20();
|
||||
append($$anchor4, div_18);
|
||||
};
|
||||
if_block(node_14, ($$render) => {
|
||||
if (get(civitaiLoading) && !get(civitaiResults).length) $$render(consequent_9);
|
||||
else if (get(civitaiLoading)) $$render(consequent_10, 1);
|
||||
else if (get(civitaiHasSearched) && !get(civitaiResults).length) $$render(consequent_11, 2);
|
||||
});
|
||||
}
|
||||
append($$anchor3, fragment_3);
|
||||
append($$anchor3, fragment_4);
|
||||
};
|
||||
var d = user_derived(() => get(searchQuery).trim() && (get(civitaiResults).length || get(civitaiLoading) || get(civitaiHasSearched)));
|
||||
if_block(node_10, ($$render) => {
|
||||
if (get(d)) $$render(consequent_10);
|
||||
if_block(node_12, ($$render) => {
|
||||
if (get(d)) $$render(consequent_12);
|
||||
});
|
||||
}
|
||||
var node_13 = sibling(node_10, 2);
|
||||
var node_15 = sibling(node_12, 2);
|
||||
{
|
||||
var consequent_11 = ($$anchor3) => {
|
||||
var div_18 = root_18();
|
||||
append($$anchor3, div_18);
|
||||
var consequent_13 = ($$anchor3) => {
|
||||
var div_19 = root_21();
|
||||
append($$anchor3, div_19);
|
||||
};
|
||||
if_block(node_13, ($$render) => {
|
||||
if (!get(filteredGroups).length && !get(catalogInstalled).length && !get(catalogUninstalled).length && !get(civitaiResults).length && !get(civitaiLoading) && !get(civitaiHasSearched) && !get(loading)) $$render(consequent_11);
|
||||
if_block(node_15, ($$render) => {
|
||||
if (!get(filteredGroups).length && !get(catalogInstalled).length && !get(catalogUninstalled).length && !get(civitaiResults).length && !get(civitaiLoading) && !get(civitaiHasSearched) && !get(loading)) $$render(consequent_13);
|
||||
});
|
||||
}
|
||||
bind_this(div_4, ($$value) => listEl = $$value, () => listEl);
|
||||
@@ -972,61 +1071,61 @@ function ModelPicker($$anchor, $$props) {
|
||||
});
|
||||
}
|
||||
bind_this(div, ($$value) => pickerEl = $$value, () => pickerEl);
|
||||
var node_14 = sibling(div, 2);
|
||||
var node_16 = sibling(div, 2);
|
||||
{
|
||||
var consequent_12 = ($$anchor2) => {
|
||||
var consequent_14 = ($$anchor2) => {
|
||||
const group = user_derived(() => get(activeSubmenu).group);
|
||||
var div_19 = root_19();
|
||||
var node_15 = sibling(child(div_19), 2);
|
||||
each(node_15, 17, () => get(group).versions, index, ($$anchor3, model) => {
|
||||
var div_20 = root_22();
|
||||
var node_17 = sibling(child(div_20), 2);
|
||||
each(node_17, 17, () => get(group).versions, index, ($$anchor3, model) => {
|
||||
const label = user_derived(() => buildVersionLabel(get(model)));
|
||||
var div_20 = root_20();
|
||||
let classes_4;
|
||||
var span_11 = child(div_20);
|
||||
var text_13 = child(span_11);
|
||||
var div_21 = root_23();
|
||||
let classes_6;
|
||||
var span_12 = child(div_21);
|
||||
var text_15 = child(span_12);
|
||||
template_effect(() => {
|
||||
var _a, _b;
|
||||
classes_4 = set_class(div_20, 1, "pcr-picker-version-item svelte-ycwcax", null, classes_4, {
|
||||
classes_6 = set_class(div_21, 1, "pcr-picker-version-item svelte-ycwcax", null, classes_6, {
|
||||
"pcr-submenu-open": ((_b = (_a = get(activeNestedSubmenu)) == null ? void 0 : _a.model) == null ? void 0 : _b.hash) === get(model).hash
|
||||
});
|
||||
set_text(text_13, get(label));
|
||||
set_text(text_15, get(label));
|
||||
});
|
||||
delegated("click", div_20, () => handleVersionClick(get(model), get(label)));
|
||||
append($$anchor3, div_20);
|
||||
delegated("click", div_21, () => handleVersionClick(get(model), get(label)));
|
||||
append($$anchor3, div_21);
|
||||
});
|
||||
template_effect(() => set_style(div_19, get(submenuStyle)));
|
||||
append($$anchor2, div_19);
|
||||
};
|
||||
if_block(node_14, ($$render) => {
|
||||
var _a;
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "version") $$render(consequent_12);
|
||||
});
|
||||
}
|
||||
var node_16 = sibling(node_14, 2);
|
||||
{
|
||||
var consequent_13 = ($$anchor2) => {
|
||||
const group = user_derived(() => get(activeSubmenu).group);
|
||||
var div_21 = root_21();
|
||||
var node_17 = sibling(child(div_21), 2);
|
||||
each(node_17, 17, () => get(group).versions, index, ($$anchor3, entry) => {
|
||||
var div_22 = root_22();
|
||||
var span_12 = child(div_22);
|
||||
var text_14 = child(span_12);
|
||||
template_effect(($0) => set_text(text_14, $0), [() => buildCatalogVersionLabel(get(entry))]);
|
||||
delegated("click", div_22, () => handleCatalogVersionClick(get(entry)));
|
||||
append($$anchor3, div_22);
|
||||
});
|
||||
template_effect(() => set_style(div_21, get(submenuStyle)));
|
||||
append($$anchor2, div_21);
|
||||
template_effect(() => set_style(div_20, get(submenuStyle)));
|
||||
append($$anchor2, div_20);
|
||||
};
|
||||
if_block(node_16, ($$render) => {
|
||||
var _a;
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "catalogVersion") $$render(consequent_13);
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "version") $$render(consequent_14);
|
||||
});
|
||||
}
|
||||
var node_18 = sibling(node_16, 2);
|
||||
{
|
||||
var consequent_19 = ($$anchor2) => {
|
||||
var consequent_15 = ($$anchor2) => {
|
||||
const group = user_derived(() => get(activeSubmenu).group);
|
||||
var div_22 = root_24();
|
||||
var node_19 = sibling(child(div_22), 2);
|
||||
each(node_19, 17, () => get(group).versions, index, ($$anchor3, entry) => {
|
||||
var div_23 = root_25();
|
||||
var span_13 = child(div_23);
|
||||
var text_16 = child(span_13);
|
||||
template_effect(($0) => set_text(text_16, $0), [() => buildCatalogVersionLabel(get(entry))]);
|
||||
delegated("click", div_23, () => handleCatalogVersionClick(get(entry)));
|
||||
append($$anchor3, div_23);
|
||||
});
|
||||
template_effect(() => set_style(div_22, get(submenuStyle)));
|
||||
append($$anchor2, div_22);
|
||||
};
|
||||
if_block(node_18, ($$render) => {
|
||||
var _a;
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "catalogVersion") $$render(consequent_15);
|
||||
});
|
||||
}
|
||||
var node_20 = sibling(node_18, 2);
|
||||
{
|
||||
var consequent_21 = ($$anchor2) => {
|
||||
const model = user_derived(() => {
|
||||
var _a, _b;
|
||||
return ((_a = get(activeNestedSubmenu)) == null ? void 0 : _a.model) || ((_b = get(activeSubmenu)) == null ? void 0 : _b.model);
|
||||
@@ -1036,106 +1135,106 @@ function ModelPicker($$anchor, $$props) {
|
||||
var _a, _b;
|
||||
return ((_a = get(activeNestedSubmenu)) == null ? void 0 : _a.versionLabel) || ((_b = get(activeSubmenu)) == null ? void 0 : _b.displayName) || "";
|
||||
});
|
||||
var div_23 = root_23();
|
||||
var div_24 = child(div_23);
|
||||
var text_15 = child(div_24);
|
||||
var node_19 = sibling(div_24, 2);
|
||||
var div_24 = root_26();
|
||||
var div_25 = child(div_24);
|
||||
var text_17 = child(div_25);
|
||||
var node_21 = sibling(div_25, 2);
|
||||
{
|
||||
var consequent_15 = ($$anchor3) => {
|
||||
var fragment_4 = root_24();
|
||||
var div_25 = first_child(fragment_4);
|
||||
var node_20 = sibling(div_25, 2);
|
||||
var consequent_17 = ($$anchor3) => {
|
||||
var fragment_5 = root_27();
|
||||
var div_26 = first_child(fragment_5);
|
||||
var node_22 = sibling(div_26, 2);
|
||||
{
|
||||
var consequent_14 = ($$anchor4) => {
|
||||
var div_26 = root_25();
|
||||
append($$anchor4, div_26);
|
||||
var consequent_16 = ($$anchor4) => {
|
||||
var div_27 = root_28();
|
||||
append($$anchor4, div_27);
|
||||
};
|
||||
if_block(node_20, ($$render) => {
|
||||
if (get(tpls).length) $$render(consequent_14);
|
||||
if_block(node_22, ($$render) => {
|
||||
if (get(tpls).length) $$render(consequent_16);
|
||||
});
|
||||
}
|
||||
delegated("click", div_25, () => {
|
||||
delegated("click", div_26, () => {
|
||||
var _a;
|
||||
onSwapInPlace()(get(model).filename, get(model).hash);
|
||||
(_a = $$props.onClose) == null ? void 0 : _a.call($$props);
|
||||
});
|
||||
append($$anchor3, fragment_4);
|
||||
};
|
||||
var d_1 = user_derived(() => canSwapInPlace(get(model)));
|
||||
if_block(node_19, ($$render) => {
|
||||
if (get(d_1)) $$render(consequent_15);
|
||||
});
|
||||
}
|
||||
var node_21 = sibling(node_19, 2);
|
||||
{
|
||||
var consequent_18 = ($$anchor3) => {
|
||||
const cats = user_derived(() => [...new Set(get(tpls).map((t) => t.category || "General"))]);
|
||||
const sortedCats = user_derived(() => sortCategories(get(cats)));
|
||||
var fragment_5 = comment();
|
||||
var node_22 = first_child(fragment_5);
|
||||
each(node_22, 17, () => get(sortedCats), index, ($$anchor4, cat) => {
|
||||
const catTpls = user_derived(() => get(tpls).filter((t) => (t.category || "General") === get(cat)));
|
||||
var fragment_6 = comment();
|
||||
var node_23 = first_child(fragment_6);
|
||||
{
|
||||
var consequent_17 = ($$anchor5) => {
|
||||
var fragment_7 = root_28();
|
||||
var node_24 = first_child(fragment_7);
|
||||
{
|
||||
var consequent_16 = ($$anchor6) => {
|
||||
var div_27 = root_29();
|
||||
var text_16 = child(div_27);
|
||||
template_effect(($0) => set_text(text_16, $0), [() => get(cat).toUpperCase()]);
|
||||
append($$anchor6, div_27);
|
||||
};
|
||||
if_block(node_24, ($$render) => {
|
||||
if (get(sortedCats).length > 1) $$render(consequent_16);
|
||||
});
|
||||
}
|
||||
var node_25 = sibling(node_24, 2);
|
||||
each(node_25, 17, () => get(catTpls), index, ($$anchor6, tpl) => {
|
||||
var div_28 = root_30();
|
||||
var text_17 = child(div_28);
|
||||
template_effect(() => {
|
||||
set_attribute(div_28, "title", get(tpl).description || "");
|
||||
set_text(text_17, get(tpl).name);
|
||||
});
|
||||
delegated("click", div_28, () => handleTemplateClick(get(tpl), get(model)));
|
||||
append($$anchor6, div_28);
|
||||
});
|
||||
append($$anchor5, fragment_7);
|
||||
};
|
||||
if_block(node_23, ($$render) => {
|
||||
if (get(catTpls).length) $$render(consequent_17);
|
||||
});
|
||||
}
|
||||
append($$anchor4, fragment_6);
|
||||
});
|
||||
append($$anchor3, fragment_5);
|
||||
};
|
||||
var alternate_1 = ($$anchor3) => {
|
||||
var div_29 = root_31();
|
||||
append($$anchor3, div_29);
|
||||
};
|
||||
var d_1 = user_derived(() => canSwapInPlace(get(model)));
|
||||
if_block(node_21, ($$render) => {
|
||||
if (get(tpls).length) $$render(consequent_18);
|
||||
if (get(d_1)) $$render(consequent_17);
|
||||
});
|
||||
}
|
||||
var node_23 = sibling(node_21, 2);
|
||||
{
|
||||
var consequent_20 = ($$anchor3) => {
|
||||
const cats = user_derived(() => [...new Set(get(tpls).map((t) => t.category || "General"))]);
|
||||
const sortedCats = user_derived(() => sortCategories(get(cats)));
|
||||
var fragment_6 = comment();
|
||||
var node_24 = first_child(fragment_6);
|
||||
each(node_24, 17, () => get(sortedCats), index, ($$anchor4, cat) => {
|
||||
const catTpls = user_derived(() => get(tpls).filter((t) => (t.category || "General") === get(cat)));
|
||||
var fragment_7 = comment();
|
||||
var node_25 = first_child(fragment_7);
|
||||
{
|
||||
var consequent_19 = ($$anchor5) => {
|
||||
var fragment_8 = root_31();
|
||||
var node_26 = first_child(fragment_8);
|
||||
{
|
||||
var consequent_18 = ($$anchor6) => {
|
||||
var div_28 = root_32();
|
||||
var text_18 = child(div_28);
|
||||
template_effect(($0) => set_text(text_18, $0), [() => get(cat).toUpperCase()]);
|
||||
append($$anchor6, div_28);
|
||||
};
|
||||
if_block(node_26, ($$render) => {
|
||||
if (get(sortedCats).length > 1) $$render(consequent_18);
|
||||
});
|
||||
}
|
||||
var node_27 = sibling(node_26, 2);
|
||||
each(node_27, 17, () => get(catTpls), index, ($$anchor6, tpl) => {
|
||||
var div_29 = root_33();
|
||||
var text_19 = child(div_29);
|
||||
template_effect(() => {
|
||||
set_attribute(div_29, "title", get(tpl).description || "");
|
||||
set_text(text_19, get(tpl).name);
|
||||
});
|
||||
delegated("click", div_29, () => handleTemplateClick(get(tpl), get(model)));
|
||||
append($$anchor6, div_29);
|
||||
});
|
||||
append($$anchor5, fragment_8);
|
||||
};
|
||||
if_block(node_25, ($$render) => {
|
||||
if (get(catTpls).length) $$render(consequent_19);
|
||||
});
|
||||
}
|
||||
append($$anchor4, fragment_7);
|
||||
});
|
||||
append($$anchor3, fragment_6);
|
||||
};
|
||||
var alternate_1 = ($$anchor3) => {
|
||||
var div_30 = root_34();
|
||||
append($$anchor3, div_30);
|
||||
};
|
||||
if_block(node_23, ($$render) => {
|
||||
if (get(tpls).length) $$render(consequent_20);
|
||||
else $$render(alternate_1, -1);
|
||||
});
|
||||
}
|
||||
template_effect(() => {
|
||||
set_style(div_23, get(activeNestedSubmenu) ? get(nestedSubmenuStyle) : get(submenuStyle));
|
||||
set_text(text_15, get(displayName));
|
||||
set_style(div_24, get(activeNestedSubmenu) ? get(nestedSubmenuStyle) : get(submenuStyle));
|
||||
set_text(text_17, get(displayName));
|
||||
});
|
||||
append($$anchor2, div_23);
|
||||
append($$anchor2, div_24);
|
||||
};
|
||||
if_block(node_18, ($$render) => {
|
||||
if_block(node_20, ($$render) => {
|
||||
var _a;
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "template" || get(activeNestedSubmenu)) $$render(consequent_19);
|
||||
if (((_a = get(activeSubmenu)) == null ? void 0 : _a.type) === "template" || get(activeNestedSubmenu)) $$render(consequent_21);
|
||||
});
|
||||
}
|
||||
var node_26 = sibling(node_18, 2);
|
||||
var node_28 = sibling(node_20, 2);
|
||||
bind_this(
|
||||
HoverCard(node_26, {
|
||||
HoverCard(node_28, {
|
||||
get pickerEl() {
|
||||
return pickerEl;
|
||||
}
|
||||
@@ -1481,6 +1580,7 @@ var root_10$1 = from_html(`<button class="pcr-download-btn svelte-n0tzlo"> </but
|
||||
var root$1 = from_html(`<div class="pcr-download-overlay svelte-n0tzlo"><div class="pcr-download-modal svelte-n0tzlo"><div class="pcr-download-header svelte-n0tzlo"><div class="pcr-download-title svelte-n0tzlo"> </div> <div class="pcr-download-meta svelte-n0tzlo"> </div> <!></div> <!> <div class="pcr-download-body svelte-n0tzlo"></div> <!> <div class="pcr-download-footer svelte-n0tzlo"><div class="pcr-download-status svelte-n0tzlo"> </div> <div class="pcr-download-buttons svelte-n0tzlo"><!> <!></div></div></div></div>`);
|
||||
function CatalogDownloadModal($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let initialPrecision = prop($$props, "initialPrecision", 3, null);
|
||||
function resolveFileDownloadUrl(file) {
|
||||
if (file.download_url) return file.download_url;
|
||||
const source = file.source;
|
||||
@@ -1518,7 +1618,7 @@ function CatalogDownloadModal($$anchor, $$props) {
|
||||
}
|
||||
const rawFiles = $$props.catalogEntry.files || [];
|
||||
const precisions = extractPrecisions(rawFiles);
|
||||
let selectedPrecision = state(proxy(precisions[0] || null));
|
||||
let selectedPrecision = state(proxy(initialPrecision() && precisions.includes(initialPrecision()) ? initialPrecision() : precisions[0] || null));
|
||||
let resolvedFiles = user_derived(() => get(selectedPrecision) ? resolveFilesForPrecision(rawFiles, get(selectedPrecision)) : rawFiles);
|
||||
let fileStatuses = state(proxy({}));
|
||||
let downloading = state(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-DOP7d958.js";
|
||||
import { F as FAMILIES, A as ARCHITECTURES } from "./model-constants-cVxhUf51.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";
|
||||
@@ -384,6 +384,17 @@ function SettingsTab($$anchor, $$props) {
|
||||
if (!get(editors)[nodeType]) get(editors)[nodeType] = {};
|
||||
get(editors)[nodeType][widgetName] = val;
|
||||
}
|
||||
function mirrorIdeogramScheduler(latentNode) {
|
||||
const sched = detected().find((d) => d.type === "Ideogram4Scheduler");
|
||||
if (!sched) return;
|
||||
for (const dim of ["width", "height"]) {
|
||||
const v = $$props.readWidgetValue(latentNode, dim);
|
||||
if (v !== void 0) {
|
||||
$$props.writeWidgetValue(sched.node, dim, v);
|
||||
setEditorValue("Ideogram4Scheduler", dim, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
function setRangeValue(nodeType, widgetName, lo, hi) {
|
||||
if (!rangeValues[nodeType]) rangeValues[nodeType] = {};
|
||||
rangeValues[nodeType][`${widgetName}_range`] = [lo, hi];
|
||||
@@ -397,6 +408,17 @@ function SettingsTab($$anchor, $$props) {
|
||||
function togglePresetMenu(nodeType) {
|
||||
presetMenuOpen[nodeType] = !presetMenuOpen[nodeType];
|
||||
}
|
||||
user_effect(() => {
|
||||
function onDocDown(e) {
|
||||
var _a2, _b2;
|
||||
if ((_b2 = (_a2 = e.target) == null ? void 0 : _a2.closest) == null ? void 0 : _b2.call(_a2, ".pcr-resolution-preset-wrap")) return;
|
||||
for (const k of Object.keys(presetMenuOpen)) {
|
||||
if (presetMenuOpen[k]) presetMenuOpen[k] = false;
|
||||
}
|
||||
}
|
||||
document.addEventListener("pointerdown", onDocDown, true);
|
||||
return () => document.removeEventListener("pointerdown", onDocDown, true);
|
||||
});
|
||||
let expandedState = proxy({ ...expandedSections() });
|
||||
function toggleSection(type) {
|
||||
expandedState[type] = !expandedState[type];
|
||||
@@ -557,13 +579,23 @@ function SettingsTab($$anchor, $$props) {
|
||||
var node_8 = first_child(fragment_2);
|
||||
{
|
||||
var consequent_8 = ($$anchor4) => {
|
||||
const w = user_derived(() => $$props.readWidgetValue(node(), "width"));
|
||||
const h = user_derived(() => $$props.readWidgetValue(node(), "height"));
|
||||
const w = user_derived(() => {
|
||||
var _a2;
|
||||
return (_a2 = get(editors)[type()]) == null ? void 0 : _a2.width;
|
||||
});
|
||||
const h = user_derived(() => {
|
||||
var _a2;
|
||||
return (_a2 = get(editors)[type()]) == null ? void 0 : _a2.height;
|
||||
});
|
||||
var fragment_3 = comment();
|
||||
var node_9 = first_child(fragment_3);
|
||||
{
|
||||
var consequent_7 = ($$anchor5) => {
|
||||
const presets = user_derived(() => get(savedWidgets)["_resolutions"] || []);
|
||||
const presets = user_derived(() => {
|
||||
var _a2;
|
||||
return ((_a2 = get(editors)[type()]) == null ? void 0 : _a2["_resolutions"]) || [];
|
||||
});
|
||||
const isPreset = user_derived(() => get(presets).some((p) => p.width === get(w) && p.height === get(h)));
|
||||
var div_6 = root_10$2();
|
||||
var div_7 = sibling(child(div_6), 2);
|
||||
var div_8 = child(div_7);
|
||||
@@ -602,6 +634,7 @@ function SettingsTab($$anchor, $$props) {
|
||||
$$props.writeWidgetValue(node(), "height", get(p).height);
|
||||
setEditorValue(type(), "width", get(p).width);
|
||||
setEditorValue(type(), "height", get(p).height);
|
||||
mirrorIdeogramScheduler(node());
|
||||
presetMenuOpen[type()] = false;
|
||||
});
|
||||
delegated("click", span_2, (e) => {
|
||||
@@ -622,22 +655,13 @@ function SettingsTab($$anchor, $$props) {
|
||||
var div_12 = sibling(node_11, 2);
|
||||
let classes_2;
|
||||
var text_6 = child(div_12);
|
||||
template_effect(
|
||||
($0, $1) => {
|
||||
classes_2 = set_class(div_12, 1, "pcr-resolution-preset-add svelte-82atyh", null, classes_2, $0);
|
||||
set_text(text_6, $1);
|
||||
},
|
||||
[
|
||||
() => ({
|
||||
"pcr-resolution-preset-add-disabled": get(presets).some((p) => p.width === get(w) && p.height === get(h))
|
||||
}),
|
||||
() => get(presets).some((p) => p.width === get(w) && p.height === get(h)) ? `${get(w)}×${get(h)} already saved` : "+ Add Current"
|
||||
]
|
||||
);
|
||||
template_effect(() => {
|
||||
classes_2 = set_class(div_12, 1, "pcr-resolution-preset-add svelte-82atyh", null, classes_2, { "pcr-resolution-preset-add-disabled": get(isPreset) });
|
||||
set_text(text_6, get(isPreset) ? `${get(w)}×${get(h)} already saved` : "+ Add Current");
|
||||
});
|
||||
delegated("click", div_12, () => {
|
||||
if (!get(presets).some((p) => p.width === get(w) && p.height === get(h))) {
|
||||
get(presets).push({ width: get(w), height: get(h) });
|
||||
get(editors)[type()]["_resolutions"] = [...get(presets)];
|
||||
if (!get(isPreset)) {
|
||||
get(editors)[type()]["_resolutions"] = [...get(presets), { width: get(w), height: get(h) }];
|
||||
}
|
||||
presetMenuOpen[type()] = false;
|
||||
});
|
||||
@@ -653,28 +677,23 @@ function SettingsTab($$anchor, $$props) {
|
||||
var input_2 = sibling(input_1, 4);
|
||||
let classes_4;
|
||||
set_attribute(input_2, "step", 8);
|
||||
template_effect(
|
||||
($0, $1, $2, $3) => {
|
||||
classes = set_class(button_1, 1, "pcr-resolution-preset-btn svelte-82atyh", null, classes, $0);
|
||||
set_text(text_3, $1);
|
||||
classes_3 = set_class(input_1, 1, "pcr-resolution-input svelte-82atyh", null, classes_3, $2);
|
||||
set_value(input_1, get(w));
|
||||
classes_4 = set_class(input_2, 1, "pcr-resolution-input svelte-82atyh", null, classes_4, $3);
|
||||
set_value(input_2, get(h));
|
||||
},
|
||||
[
|
||||
() => ({
|
||||
"pcr-resolution-preset-active": get(presets).some((p) => p.width === get(w) && p.height === get(h))
|
||||
}),
|
||||
() => get(presets).some((p) => p.width === get(w) && p.height === get(h)) ? `${get(w)}×${get(h)}` : "Presets",
|
||||
() => ({
|
||||
"pcr-resolution-input-dimmed": get(presets).some((p) => p.width === get(w) && p.height === get(h))
|
||||
}),
|
||||
() => ({
|
||||
"pcr-resolution-input-dimmed": get(presets).some((p) => p.width === get(w) && p.height === get(h))
|
||||
})
|
||||
]
|
||||
);
|
||||
template_effect(() => {
|
||||
classes = set_class(button_1, 1, "pcr-resolution-preset-btn svelte-82atyh", null, classes, {
|
||||
"pcr-resolution-preset-active": get(isPreset),
|
||||
"pcr-resolution-preset-dimmed": !get(isPreset)
|
||||
});
|
||||
set_text(text_3, get(isPreset) ? `${get(w)}×${get(h)}` : "Presets");
|
||||
classes_3 = set_class(input_1, 1, "pcr-resolution-input svelte-82atyh", null, classes_3, {
|
||||
"pcr-resolution-input-dimmed": get(isPreset),
|
||||
"pcr-resolution-input-active": !get(isPreset)
|
||||
});
|
||||
set_value(input_1, get(w));
|
||||
classes_4 = set_class(input_2, 1, "pcr-resolution-input svelte-82atyh", null, classes_4, {
|
||||
"pcr-resolution-input-dimmed": get(isPreset),
|
||||
"pcr-resolution-input-active": !get(isPreset)
|
||||
});
|
||||
set_value(input_2, get(h));
|
||||
});
|
||||
delegated("click", button_1, (e) => {
|
||||
e.stopPropagation();
|
||||
togglePresetMenu(type());
|
||||
@@ -683,11 +702,13 @@ function SettingsTab($$anchor, $$props) {
|
||||
const v = parseInt(e.target.value) || 512;
|
||||
setEditorValue(type(), "width", v);
|
||||
$$props.writeWidgetValue(node(), "width", v);
|
||||
mirrorIdeogramScheduler(node());
|
||||
});
|
||||
delegated("change", input_2, (e) => {
|
||||
const v = parseInt(e.target.value) || 512;
|
||||
setEditorValue(type(), "height", v);
|
||||
$$props.writeWidgetValue(node(), "height", v);
|
||||
mirrorIdeogramScheduler(node());
|
||||
});
|
||||
append($$anchor5, div_6);
|
||||
};
|
||||
|
||||
@@ -4,19 +4,19 @@ var __typeError = (msg) => {
|
||||
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
||||
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
||||
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
||||
var _mode, _switchIndex, _locked, _disabled, _collapsed, _connectedCount, _hasLabels, _switchLabel, _iterateCurrent, _iterateTotal, _iterateCycle, _switchOptions, _outputPanelOpen, _imagePanelVisible, _aiAssistantOpen, _posePanelVisible, _hasPoseStudio, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _rollSelected;
|
||||
var _mode, _switchIndex, _locked, _disabled, _collapsed, _connectedCount, _hasLabels, _switchLabel, _iterateCurrent, _iterateTotal, _iterateCycle, _switchOptions, _outputPanelOpen, _imagePanelVisible, _aiAssistantOpen, _posePanelVisible, _regionPanelVisible, _hasPoseStudio, _hasRegionBox, _compiledOutput, _compiledNegOutput, _imageUrl, _previewUrl, _progress, _isGenerating, _rollSelected;
|
||||
import { p as push, o as bind_this, k as append, l as pop, A as from_html, d as delegate, a as prop, g as get, j as delegated, m as user_derived, s as state, c as proxy, u as user_effect, i as if_block, f as sibling, r as event, e as set, C as tick, n as child, D as comment, v as first_child, w as each, t as template_effect, y as set_text, h as set_class, x as set_attribute, Q as clsx, z as index, R as text, E as untrack, M as unmount, L as mount } from "./disclose-version-uq4tn5Y6.js";
|
||||
import { u as useApi, p as provideApi } from "./api-context-BFKo1mCD.js";
|
||||
import { M as Menubar } from "./Menubar-DI-z1NzN.js";
|
||||
import { M as Menubar } from "./Menubar-DC12j8gE.js";
|
||||
import { o as onDestroy, a as onMount } from "./index-client-iMRCrpBY.js";
|
||||
import { s as set_style } from "./style-Boi27oOu.js";
|
||||
import { h as html, P as PopupAnchor } from "./PopupAnchor-C6OWGGTI.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-Dw0CC3-N.js";
|
||||
import { c as clampGalleryRowHeight, z as zoomGalleryRowHeight, G as GeneratedGallery } from "./GeneratedGallery-D5XbkMk7.js";
|
||||
import { b as bind_value } from "./input-DFQhebEz.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
import { r as readNodePrompt, c as cryptoId, e as extractNGrams, m as matchCharactersInDb, i as isStandaloneMainPromptChain, a as applyPromptText } from "./ai-patch-helpers-Bayqv0oF.js";
|
||||
var root$c = from_html(`<div class="pcr-editor-frame"></div>`);
|
||||
var root$d = from_html(`<div class="pcr-editor-frame"></div>`);
|
||||
function EditorPane($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let editorContainer;
|
||||
@@ -26,12 +26,12 @@ function EditorPane($$anchor, $$props) {
|
||||
function setEditorView(view) {
|
||||
}
|
||||
var $$exports = { getContainer, setEditorView };
|
||||
var div = root$c();
|
||||
var div = root$d();
|
||||
bind_this(div, ($$value) => editorContainer = $$value, () => editorContainer);
|
||||
append($$anchor, div);
|
||||
return pop($$exports);
|
||||
}
|
||||
var root$b = from_html(`<div class="pcr-output-panel-content pcr-scrollable svelte-qbaa69" tabindex="0"></div>`);
|
||||
var root$c = from_html(`<div class="pcr-output-panel-content pcr-scrollable svelte-qbaa69" tabindex="0"></div>`);
|
||||
function PromptOutput($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let compiledOutput = prop($$props, "compiledOutput", 3, ""), compiledNegOutput = prop($$props, "compiledNegOutput", 3, "");
|
||||
@@ -59,7 +59,7 @@ ${escapeHtml(compiledNegOutput())}`;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
var div = root$b();
|
||||
var div = root$c();
|
||||
html(div, () => get(html$1), true);
|
||||
delegated("pointerdown", div, (e) => e.stopPropagation());
|
||||
delegated("mousedown", div, (e) => e.stopPropagation());
|
||||
@@ -70,7 +70,7 @@ delegate(["pointerdown", "mousedown"]);
|
||||
var root_1$6 = from_html(`<span class="pcr-console-placeholder svelte-1ouufnb">No log entries</span>`);
|
||||
var root_3$5 = from_html(`<div class="pcr-console-line svelte-1ouufnb"><span class="pcr-console-time svelte-1ouufnb"> </span> <span class="pcr-console-msg svelte-1ouufnb"> </span></div>`);
|
||||
var root_4$6 = from_html(`<div class="pcr-console-scroll-btn svelte-1ouufnb"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"></path></svg></div>`);
|
||||
var root$a = from_html(`<div class="pcr-console-log pcr-scrollable svelte-1ouufnb" tabindex="0"><!> <!></div>`);
|
||||
var root$b = from_html(`<div class="pcr-console-log pcr-scrollable svelte-1ouufnb" tabindex="0"><!> <!></div>`);
|
||||
function ConsoleLog($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let active = prop($$props, "active", 3, false);
|
||||
@@ -155,7 +155,7 @@ function ConsoleLog($$anchor, $$props) {
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
var div = root$a();
|
||||
var div = root$b();
|
||||
var node = child(div);
|
||||
{
|
||||
var consequent = ($$anchor2) => {
|
||||
@@ -212,8 +212,8 @@ function ConsoleLog($$anchor, $$props) {
|
||||
pop();
|
||||
}
|
||||
delegate(["pointerdown", "mousedown", "click"]);
|
||||
var root_2$8 = from_html(`<div class="pcr-output-panel-generated pcr-scrollable svelte-1kzsw09" style="display:block"><!></div>`);
|
||||
var root$9 = from_html(`<div></div> <div class="pcr-output-panel svelte-1kzsw09"><div class="pcr-output-panel-header svelte-1kzsw09"><div class="pcr-output-panel-tabs svelte-1kzsw09"><button>Prompt Output</button> <button> </button> <button>Console</button></div> <div class="pcr-output-panel-controls svelte-1kzsw09"><div class="pcr-output-panel-maximize svelte-1kzsw09"></div> <div class="pcr-output-panel-close svelte-1kzsw09" title="Close Panel"></div></div></div> <!> <div><!></div> <!></div>`, 1);
|
||||
var root_2$9 = from_html(`<div class="pcr-output-panel-generated pcr-scrollable svelte-1kzsw09" style="display:block"><!></div>`);
|
||||
var root$a = from_html(`<div></div> <div class="pcr-output-panel svelte-1kzsw09"><div class="pcr-output-panel-header svelte-1kzsw09"><div class="pcr-output-panel-tabs svelte-1kzsw09"><button>Prompt Output</button> <button> </button> <button>Console</button></div> <div class="pcr-output-panel-controls svelte-1kzsw09"><div class="pcr-output-panel-maximize svelte-1kzsw09"></div> <div class="pcr-output-panel-close svelte-1kzsw09" title="Close Panel"></div></div></div> <!> <div><!></div> <!></div>`, 1);
|
||||
function OutputPanel($$anchor, $$props) {
|
||||
var _a, _b, _c, _d, _e;
|
||||
push($$props, true);
|
||||
@@ -486,7 +486,7 @@ function OutputPanel($$anchor, $$props) {
|
||||
});
|
||||
onDestroy(() => resizeAc == null ? void 0 : resizeAc.abort());
|
||||
var $$exports = { open, close, toggle, openPrompt, getIsOpen };
|
||||
var fragment = root$9();
|
||||
var fragment = root$a();
|
||||
var div = first_child(fragment);
|
||||
let classes;
|
||||
let styles;
|
||||
@@ -537,7 +537,7 @@ function OutputPanel($$anchor, $$props) {
|
||||
var node_3 = sibling(div_7, 2);
|
||||
{
|
||||
var consequent_2 = ($$anchor2) => {
|
||||
var div_8 = root_2$8();
|
||||
var div_8 = root_2$9();
|
||||
var node_4 = child(div_8);
|
||||
{
|
||||
var consequent_1 = ($$anchor3) => {
|
||||
@@ -674,12 +674,13 @@ function OutputPanel($$anchor, $$props) {
|
||||
return pop($$exports);
|
||||
}
|
||||
delegate(["pointerdown", "mousedown", "click"]);
|
||||
var root_1$5 = from_html(`<img draggable="false" alt="" class="svelte-ro1n43"/>`);
|
||||
var root_2$7 = from_html(`<div class="pcr-image-placeholder svelte-ro1n43"> </div>`);
|
||||
var root_4$5 = from_html(`<span class="pcr-image-panel-progress-text svelte-ro1n43"> </span>`);
|
||||
var root_3$4 = from_html(`<div><div class="pcr-image-panel-progress-track svelte-ro1n43"><div class="pcr-image-panel-progress-bar svelte-ro1n43"></div></div> <!></div>`);
|
||||
var root_5$4 = from_html(`<span class="pcr-image-panel-info-text svelte-ro1n43"> </span>`);
|
||||
var root$8 = from_html(`<div class="pcr-image-divider svelte-ro1n43"></div> <div class="pcr-image-panel svelte-ro1n43"><div class="pcr-image-panel-header svelte-ro1n43"><span> </span> <button class="pcr-image-panel-close-btn svelte-ro1n43" title="Close image panel"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svelte-ro1n43"><line x1="18" y1="6" x2="6" y2="18" class="svelte-ro1n43"></line><line x1="6" y1="6" x2="18" y2="18" class="svelte-ro1n43"></line></svg></button></div> <div class="pcr-image-container svelte-ro1n43"><!> <!></div> <div><!></div></div>`, 1);
|
||||
var root_1$5 = from_html(`<video autoplay="" loop="" playsinline="" controls="" class="svelte-ro1n43"></video>`, 2);
|
||||
var root_2$8 = from_html(`<img draggable="false" alt="" class="svelte-ro1n43"/>`);
|
||||
var root_3$4 = from_html(`<div class="pcr-image-placeholder svelte-ro1n43"> </div>`);
|
||||
var root_5$4 = from_html(`<span class="pcr-image-panel-progress-text svelte-ro1n43"> </span>`);
|
||||
var root_4$5 = from_html(`<div><div class="pcr-image-panel-progress-track svelte-ro1n43"><div class="pcr-image-panel-progress-bar svelte-ro1n43"></div></div> <!></div>`);
|
||||
var root_6$3 = from_html(`<span class="pcr-image-panel-info-text svelte-ro1n43"> </span>`);
|
||||
var root$9 = from_html(`<div class="pcr-image-divider svelte-ro1n43"></div> <div class="pcr-image-panel svelte-ro1n43"><div class="pcr-image-panel-header svelte-ro1n43"><span> </span> <button class="pcr-image-panel-close-btn svelte-ro1n43" title="Close image panel"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svelte-ro1n43"><line x1="18" y1="6" x2="6" y2="18" class="svelte-ro1n43"></line><line x1="6" y1="6" x2="18" y2="18" class="svelte-ro1n43"></line></svg></button></div> <div class="pcr-image-container svelte-ro1n43"><!> <!></div> <div><!></div></div>`, 1);
|
||||
function ImagePanel($$anchor, $$props) {
|
||||
var _a, _b;
|
||||
push($$props, true);
|
||||
@@ -699,6 +700,7 @@ function ImagePanel($$anchor, $$props) {
|
||||
let panelEl;
|
||||
let dividerEl;
|
||||
let imgEl;
|
||||
let videoEl;
|
||||
let imgContainerEl;
|
||||
let isVisible = state(!!((_a = node().properties) == null ? void 0 : _a.pcrImagePreview));
|
||||
let panelWidth = state(proxy(((_b = node().properties) == null ? void 0 : _b.pcrImagePanelWidth) || DEFAULT_WIDTH));
|
||||
@@ -771,6 +773,10 @@ function ImagePanel($$anchor, $$props) {
|
||||
return { filename: "", subfolder: "" };
|
||||
}
|
||||
}
|
||||
let isVideoUrl = user_derived(() => {
|
||||
if (!get(currentImageUrl)) return false;
|
||||
return /\.(mp4|webm|m4v|mov)(\?|$)/i.test(parseImageUrl(get(currentImageUrl)).filename);
|
||||
});
|
||||
let filenameDisplay = user_derived(() => {
|
||||
if (shared().isGenerating) return "Generating…";
|
||||
if (!get(currentImageUrl)) return "";
|
||||
@@ -828,6 +834,12 @@ function ImagePanel($$anchor, $$props) {
|
||||
set(naturalHeight, (imgEl == null ? void 0 : imgEl.naturalHeight) || 0, true);
|
||||
if (!get(isPreviewMode)) resetZoom();
|
||||
}
|
||||
function handleVideoLoaded() {
|
||||
set(imageLoaded, true);
|
||||
set(imageError, false);
|
||||
set(naturalWidth, (videoEl == null ? void 0 : videoEl.videoWidth) || 0, true);
|
||||
set(naturalHeight, (videoEl == null ? void 0 : videoEl.videoHeight) || 0, true);
|
||||
}
|
||||
function handleImageError() {
|
||||
set(imageLoaded, false);
|
||||
set(imageError, true);
|
||||
@@ -1093,7 +1105,7 @@ function ImagePanel($$anchor, $$props) {
|
||||
showProgressBar,
|
||||
hideProgress
|
||||
};
|
||||
var fragment = root$8();
|
||||
var fragment = root$9();
|
||||
var div = first_child(fragment);
|
||||
let styles;
|
||||
bind_this(div, ($$value) => dividerEl = $$value, () => dividerEl);
|
||||
@@ -1108,62 +1120,76 @@ function ImagePanel($$anchor, $$props) {
|
||||
var node_1 = child(div_3);
|
||||
{
|
||||
var consequent = ($$anchor2) => {
|
||||
var img_1 = root_1$5();
|
||||
var video = root_1$5();
|
||||
video.muted = true;
|
||||
let styles_2;
|
||||
bind_this(video, ($$value) => videoEl = $$value, () => videoEl);
|
||||
template_effect(() => {
|
||||
set_attribute(video, "src", get(currentImageUrl));
|
||||
styles_2 = set_style(video, "", styles_2, { display: get(imageLoaded) ? "block" : "none" });
|
||||
});
|
||||
event("loadeddata", video, handleVideoLoaded);
|
||||
event("error", video, handleImageError);
|
||||
append($$anchor2, video);
|
||||
};
|
||||
var consequent_1 = ($$anchor2) => {
|
||||
var img_1 = root_2$8();
|
||||
let styles_3;
|
||||
bind_this(img_1, ($$value) => imgEl = $$value, () => imgEl);
|
||||
template_effect(() => {
|
||||
set_attribute(img_1, "src", get(currentImageUrl));
|
||||
styles_2 = set_style(img_1, "", styles_2, { display: get(imageLoaded) ? "block" : "none" });
|
||||
styles_3 = set_style(img_1, "", styles_3, { display: get(imageLoaded) ? "block" : "none" });
|
||||
});
|
||||
event("load", img_1, handleImageLoad);
|
||||
event("error", img_1, handleImageError);
|
||||
append($$anchor2, img_1);
|
||||
};
|
||||
if_block(node_1, ($$render) => {
|
||||
if (get(currentImageUrl)) $$render(consequent);
|
||||
if (get(currentImageUrl) && get(isVideoUrl)) $$render(consequent);
|
||||
else if (get(currentImageUrl)) $$render(consequent_1, 1);
|
||||
});
|
||||
}
|
||||
var node_2 = sibling(node_1, 2);
|
||||
{
|
||||
var consequent_1 = ($$anchor2) => {
|
||||
var div_4 = root_2$7();
|
||||
var consequent_2 = ($$anchor2) => {
|
||||
var div_4 = root_3$4();
|
||||
var text_1 = child(div_4);
|
||||
template_effect(() => set_text(text_1, get(imageError) ? "Image not found" : "No image"));
|
||||
append($$anchor2, div_4);
|
||||
};
|
||||
if_block(node_2, ($$render) => {
|
||||
if (!get(imageLoaded) || !get(currentImageUrl)) $$render(consequent_1);
|
||||
if (!get(imageLoaded) || !get(currentImageUrl)) $$render(consequent_2);
|
||||
});
|
||||
}
|
||||
bind_this(div_3, ($$value) => imgContainerEl = $$value, () => imgContainerEl);
|
||||
var div_5 = sibling(div_3, 2);
|
||||
let classes_1;
|
||||
let styles_3;
|
||||
let styles_4;
|
||||
var node_3 = child(div_5);
|
||||
{
|
||||
var consequent_3 = ($$anchor2) => {
|
||||
var div_6 = root_3$4();
|
||||
var consequent_4 = ($$anchor2) => {
|
||||
var div_6 = root_4$5();
|
||||
let classes_2;
|
||||
var div_7 = child(div_6);
|
||||
var div_8 = child(div_7);
|
||||
let styles_4;
|
||||
let styles_5;
|
||||
var node_4 = sibling(div_7, 2);
|
||||
{
|
||||
var consequent_2 = ($$anchor3) => {
|
||||
var span_1 = root_4$5();
|
||||
var consequent_3 = ($$anchor3) => {
|
||||
var span_1 = root_5$4();
|
||||
var text_2 = child(span_1);
|
||||
template_effect(() => set_text(text_2, `${get(progressValue) ?? ""}/${get(progressMax) ?? ""}`));
|
||||
append($$anchor3, span_1);
|
||||
};
|
||||
if_block(node_4, ($$render) => {
|
||||
if (!get(progressIndeterminate)) $$render(consequent_2);
|
||||
if (!get(progressIndeterminate)) $$render(consequent_3);
|
||||
});
|
||||
}
|
||||
template_effect(() => {
|
||||
classes_2 = set_class(div_6, 1, "pcr-image-panel-progress svelte-ro1n43", null, classes_2, {
|
||||
"pcr-image-panel-progress-indeterminate": get(progressIndeterminate)
|
||||
});
|
||||
styles_4 = set_style(div_8, "", styles_4, {
|
||||
styles_5 = set_style(div_8, "", styles_5, {
|
||||
width: `${get(progressPercent) ?? ""}%`,
|
||||
transition: `width ${progressDuration * 1.3}s linear`
|
||||
});
|
||||
@@ -1171,13 +1197,13 @@ function ImagePanel($$anchor, $$props) {
|
||||
append($$anchor2, div_6);
|
||||
};
|
||||
var alternate = ($$anchor2) => {
|
||||
var span_2 = root_5$4();
|
||||
var span_2 = root_6$3();
|
||||
var text_3 = child(span_2);
|
||||
template_effect(() => set_text(text_3, get(infoText)));
|
||||
append($$anchor2, span_2);
|
||||
};
|
||||
if_block(node_3, ($$render) => {
|
||||
if (get(showProgress)) $$render(consequent_3);
|
||||
if (get(showProgress)) $$render(consequent_4);
|
||||
else $$render(alternate, -1);
|
||||
});
|
||||
}
|
||||
@@ -1192,7 +1218,7 @@ function ImagePanel($$anchor, $$props) {
|
||||
set_attribute(span, "title", get(filenameDisplay));
|
||||
set_text(text2, get(filenameDisplay));
|
||||
classes_1 = set_class(div_5, 1, "pcr-image-panel-info svelte-ro1n43", null, classes_1, { "pcr-image-panel-info-always": get(showProgress) });
|
||||
styles_3 = set_style(div_5, "", styles_3, {
|
||||
styles_4 = set_style(div_5, "", styles_4, {
|
||||
display: get(showProgress) || get(imageLoaded) && get(currentImageUrl) ? "flex" : "none"
|
||||
});
|
||||
});
|
||||
@@ -1234,8 +1260,8 @@ delegate([
|
||||
"pointermove",
|
||||
"pointerup"
|
||||
]);
|
||||
var root_2$6 = from_html(`<div class="pcr-pose-panel-placeholder svelte-1xr40xl">No 3D Poser node in the graph</div>`);
|
||||
var root$7 = from_html(`<div class="pcr-pose-panel svelte-1xr40xl"><!> <div class="pcr-pose-panel-body svelte-1xr40xl"><!></div></div> <div class="pcr-pose-divider svelte-1xr40xl"></div>`, 1);
|
||||
var root_2$7 = from_html(`<div class="pcr-pose-panel-placeholder svelte-1xr40xl">No 3D Poser node in the graph</div>`);
|
||||
var root$8 = from_html(`<div class="pcr-pose-panel svelte-1xr40xl"><!> <div class="pcr-pose-panel-body svelte-1xr40xl"><!></div></div> <div class="pcr-pose-divider svelte-1xr40xl"></div>`, 1);
|
||||
function PosePanel($$anchor, $$props) {
|
||||
var _a, _b;
|
||||
push($$props, true);
|
||||
@@ -1415,6 +1441,240 @@ function PosePanel($$anchor, $$props) {
|
||||
toggle,
|
||||
getIsVisible
|
||||
};
|
||||
var fragment = root$8();
|
||||
var div = first_child(fragment);
|
||||
let styles;
|
||||
var node_1 = child(div);
|
||||
{
|
||||
if_block(node_1, ($$render) => {
|
||||
});
|
||||
}
|
||||
var div_1 = sibling(node_1, 2);
|
||||
var node_2 = child(div_1);
|
||||
{
|
||||
var consequent_1 = ($$anchor2) => {
|
||||
var div_2 = root_2$7();
|
||||
append($$anchor2, div_2);
|
||||
};
|
||||
if_block(node_2, ($$render) => {
|
||||
if (!get(hasDock)) $$render(consequent_1);
|
||||
});
|
||||
}
|
||||
bind_this(div_1, ($$value) => bodyEl = $$value, () => bodyEl);
|
||||
bind_this(div, ($$value) => panelEl = $$value, () => panelEl);
|
||||
var div_3 = sibling(div, 2);
|
||||
let styles_1;
|
||||
bind_this(div_3, ($$value) => dividerEl = $$value, () => dividerEl);
|
||||
template_effect(() => {
|
||||
styles = set_style(div, "", styles, {
|
||||
width: `${get(panelWidth) ?? ""}px`,
|
||||
display: get(isVisible) ? "flex" : "none"
|
||||
});
|
||||
styles_1 = set_style(div_3, "", styles_1, { display: get(isVisible) ? "flex" : "none" });
|
||||
});
|
||||
delegated("pointerdown", div, (e) => e.stopPropagation());
|
||||
delegated("mousedown", div, (e) => e.stopPropagation());
|
||||
delegated("click", div, (e) => e.stopPropagation());
|
||||
delegated("dblclick", div, (e) => e.stopPropagation());
|
||||
event("pointerleave", div, onPanelLeave);
|
||||
event("pointermove", div, onPanelMove, true);
|
||||
delegated("dblclick", div_3, (e) => {
|
||||
var _a2, _b2, _c, _d;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const poser = dockedNode || getActivePoser()();
|
||||
const w = (_b2 = (_a2 = poser == null ? void 0 : poser.widgets) == null ? void 0 : _a2.find((x) => x.name === "width")) == null ? void 0 : _b2.value;
|
||||
const h = (_d = (_c = poser == null ? void 0 : poser.widgets) == null ? void 0 : _c.find((x) => x.name === "height")) == null ? void 0 : _d.value;
|
||||
if (!w || !h) return;
|
||||
const bodyH = (bodyEl == null ? void 0 : bodyEl.offsetHeight) || (panelEl == null ? void 0 : panelEl.offsetHeight) || 300;
|
||||
set(panelWidth, clampPanelWidth(Math.round(bodyH * (w / h))), true);
|
||||
if (node().properties) node().properties.pcrPosePanelWidth = get(panelWidth);
|
||||
});
|
||||
append($$anchor, fragment);
|
||||
return pop($$exports);
|
||||
}
|
||||
delegate(["pointerdown", "mousedown", "click", "dblclick"]);
|
||||
var root_2$6 = from_html(`<div class="pcr-region-panel-placeholder svelte-wos87s">No Region Box node in the graph</div>`);
|
||||
var root$7 = from_html(`<div class="pcr-region-panel svelte-wos87s"><!> <div class="pcr-region-panel-body svelte-wos87s"><!></div></div> <div class="pcr-region-divider svelte-wos87s"></div>`, 1);
|
||||
function RegionPanel($$anchor, $$props) {
|
||||
var _a, _b;
|
||||
push($$props, true);
|
||||
const DEFAULT_WIDTH = 440;
|
||||
const MIN_WIDTH = 240;
|
||||
let node = prop($$props, "node", 7), onToggle = prop($$props, "onToggle", 3, () => {
|
||||
}), onRegister = prop($$props, "onRegister", 3, null), getActiveRegionBox = prop($$props, "getActiveRegionBox", 3, () => null);
|
||||
const { getCanvasScale } = useApi();
|
||||
let panelEl;
|
||||
let dividerEl;
|
||||
let bodyEl;
|
||||
let isVisible = state(!!((_a = node().properties) == null ? void 0 : _a.pcrRegionPanel));
|
||||
let panelWidth = state(proxy(((_b = node().properties) == null ? void 0 : _b.pcrRegionPanelWidth) || DEFAULT_WIDTH));
|
||||
let dockedNode = null;
|
||||
let hasDock = state(false);
|
||||
let toggleListener = null;
|
||||
function emitToggle(visible) {
|
||||
onToggle()(visible);
|
||||
toggleListener == null ? void 0 : toggleListener(visible);
|
||||
}
|
||||
function setToggleListener(cb) {
|
||||
toggleListener = cb;
|
||||
}
|
||||
function dock(rb) {
|
||||
var _a2;
|
||||
if (!rb || !bodyEl || !((_a2 = rb._pcrRegion) == null ? void 0 : _a2.enterDock)) return;
|
||||
rb._pcrRegion.enterDock(bodyEl);
|
||||
dockedNode = rb;
|
||||
set(hasDock, true);
|
||||
}
|
||||
function undock() {
|
||||
var _a2;
|
||||
if (dockedNode && ((_a2 = dockedNode._pcrRegion) == null ? void 0 : _a2.exitDock)) {
|
||||
try {
|
||||
dockedNode._pcrRegion.exitDock();
|
||||
} catch (e) {
|
||||
console.error("[PromptChain] region undock error", e);
|
||||
}
|
||||
}
|
||||
dockedNode = null;
|
||||
set(hasDock, false);
|
||||
}
|
||||
function refreshDock() {
|
||||
if (!get(isVisible)) return;
|
||||
if (dockedNode && dockedNode._pcrAlive) return;
|
||||
if (dockedNode) undock();
|
||||
const active = getActiveRegionBox()();
|
||||
if (active) dock(active);
|
||||
}
|
||||
function show() {
|
||||
set(isVisible, true);
|
||||
if (node().properties) node().properties.pcrRegionPanel = true;
|
||||
emitToggle(true);
|
||||
requestAnimationFrame(() => {
|
||||
if (get(isVisible)) dock(getActiveRegionBox()());
|
||||
});
|
||||
}
|
||||
function hide() {
|
||||
undock();
|
||||
set(isVisible, false);
|
||||
if (node().properties) node().properties.pcrRegionPanel = false;
|
||||
emitToggle(false);
|
||||
}
|
||||
function toggle() {
|
||||
get(isVisible) ? hide() : show();
|
||||
}
|
||||
function getIsVisible() {
|
||||
return get(isVisible);
|
||||
}
|
||||
function getMaxPanelWidth() {
|
||||
const row = panelEl == null ? void 0 : panelEl.parentElement;
|
||||
const rowWidth = (row == null ? void 0 : row.offsetWidth) || 0;
|
||||
if (!rowWidth) return Number.POSITIVE_INFINITY;
|
||||
return Math.max(MIN_WIDTH, rowWidth - 120);
|
||||
}
|
||||
function clampPanelWidth(value) {
|
||||
return Math.min(Math.max(MIN_WIDTH, value), getMaxPanelWidth());
|
||||
}
|
||||
onMount(() => {
|
||||
var _a2, _b2;
|
||||
(_a2 = onRegister()) == null ? void 0 : _a2({
|
||||
toggle,
|
||||
show,
|
||||
hide,
|
||||
getIsVisible,
|
||||
refreshDock,
|
||||
setToggleListener,
|
||||
cleanup
|
||||
});
|
||||
if ((_b2 = node().properties) == null ? void 0 : _b2.pcrRegionPanel) requestAnimationFrame(() => show());
|
||||
requestAnimationFrame(() => {
|
||||
set(panelWidth, clampPanelWidth(get(panelWidth)), true);
|
||||
});
|
||||
});
|
||||
let dividerAc;
|
||||
onMount(() => {
|
||||
dividerAc = new AbortController();
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
const getScale = () => {
|
||||
const inFs = !!document.querySelector(".pcr-fs-overlay");
|
||||
return inFs ? 1 : getCanvasScale();
|
||||
};
|
||||
document.addEventListener(
|
||||
"pointerdown",
|
||||
(e) => {
|
||||
if (!dividerEl || e.target !== dividerEl && !dividerEl.contains(e.target)) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startWidth = (panelEl == null ? void 0 : panelEl.offsetWidth) || get(panelWidth);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const canvas = document.querySelector("canvas.lgraphcanvas");
|
||||
if (canvas && typeof e.pointerId === "number") {
|
||||
try {
|
||||
if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ capture: true, signal: dividerAc.signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
"pointermove",
|
||||
(e) => {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const delta = (e.clientX - startX) / getScale();
|
||||
set(panelWidth, clampPanelWidth(startWidth + delta), true);
|
||||
},
|
||||
{ capture: true, signal: dividerAc.signal }
|
||||
);
|
||||
document.addEventListener(
|
||||
"pointerup",
|
||||
() => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
if (node().properties) node().properties.pcrRegionPanelWidth = get(panelWidth);
|
||||
},
|
||||
{ capture: true, signal: dividerAc.signal }
|
||||
);
|
||||
});
|
||||
let closeState = state(
|
||||
"hidden"
|
||||
// hidden | dim | full
|
||||
);
|
||||
function onPanelMove(e) {
|
||||
if (!panelEl) return;
|
||||
const r = panelEl.getBoundingClientRect();
|
||||
const inTop = e.clientY - r.top < Math.max(32, r.height * 0.1);
|
||||
const inRight = r.right - e.clientX < Math.max(32, r.width * 0.1);
|
||||
set(closeState, inTop && inRight ? "full" : "dim", true);
|
||||
}
|
||||
function onPanelLeave() {
|
||||
set(closeState, "hidden");
|
||||
}
|
||||
function cleanup() {
|
||||
dividerAc == null ? void 0 : dividerAc.abort();
|
||||
undock();
|
||||
}
|
||||
onDestroy(() => {
|
||||
dividerAc == null ? void 0 : dividerAc.abort();
|
||||
undock();
|
||||
});
|
||||
var $$exports = {
|
||||
setToggleListener,
|
||||
refreshDock,
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
getIsVisible
|
||||
};
|
||||
var fragment = root$7();
|
||||
var div = first_child(fragment);
|
||||
let styles;
|
||||
@@ -1456,13 +1716,13 @@ function PosePanel($$anchor, $$props) {
|
||||
var _a2, _b2, _c, _d;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const poser = dockedNode || getActivePoser()();
|
||||
const w = (_b2 = (_a2 = poser == null ? void 0 : poser.widgets) == null ? void 0 : _a2.find((x) => x.name === "width")) == null ? void 0 : _b2.value;
|
||||
const h = (_d = (_c = poser == null ? void 0 : poser.widgets) == null ? void 0 : _c.find((x) => x.name === "height")) == null ? void 0 : _d.value;
|
||||
const rb = dockedNode || getActiveRegionBox()();
|
||||
const w = (_b2 = (_a2 = rb == null ? void 0 : rb.widgets) == null ? void 0 : _a2.find((x) => x.name === "width")) == null ? void 0 : _b2.value;
|
||||
const h = (_d = (_c = rb == null ? void 0 : rb.widgets) == null ? void 0 : _c.find((x) => x.name === "height")) == null ? void 0 : _d.value;
|
||||
if (!w || !h) return;
|
||||
const bodyH = (bodyEl == null ? void 0 : bodyEl.offsetHeight) || (panelEl == null ? void 0 : panelEl.offsetHeight) || 300;
|
||||
set(panelWidth, clampPanelWidth(Math.round(bodyH * (w / h))), true);
|
||||
if (node().properties) node().properties.pcrPosePanelWidth = get(panelWidth);
|
||||
if (node().properties) node().properties.pcrRegionPanelWidth = get(panelWidth);
|
||||
});
|
||||
append($$anchor, fragment);
|
||||
return pop($$exports);
|
||||
@@ -3440,7 +3700,7 @@ delegate([
|
||||
"keydown",
|
||||
"change"
|
||||
]);
|
||||
var root = from_html(`<!> <div class="pcr-node-content"><div class="pcr-editor-row"><!> <!> <div class="pcr-editor-stack"><!> <!></div> <!></div> <!></div>`, 1);
|
||||
var root = from_html(`<!> <div class="pcr-node-content"><div class="pcr-editor-row"><!> <!> <!> <div class="pcr-editor-stack"><!> <!></div> <!></div> <!></div>`, 1);
|
||||
function NodeWidget($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let shared = prop($$props, "shared", 7), onSetMode = prop($$props, "onSetMode", 3, () => {
|
||||
@@ -3451,8 +3711,9 @@ function NodeWidget($$anchor, $$props) {
|
||||
}), onToggleImage = prop($$props, "onToggleImage", 3, () => {
|
||||
}), onToggleAssistant = prop($$props, "onToggleAssistant", 3, () => {
|
||||
}), onTogglePose = prop($$props, "onTogglePose", 3, () => {
|
||||
}), onToggleRegion = prop($$props, "onToggleRegion", 3, () => {
|
||||
}), onOpenFullscreen = prop($$props, "onOpenFullscreen", 3, () => {
|
||||
}), docDropdownEl = prop($$props, "docDropdownEl", 3, null), modelIndicatorEl = prop($$props, "modelIndicatorEl", 3, null), tagsDropdownEl = prop($$props, "tagsDropdownEl", 3, null), getEditorView = prop($$props, "getEditorView", 3, () => null), onEditorPaneReady = prop($$props, "onEditorPaneReady", 3, null), onOutputPanelRegister = prop($$props, "onOutputPanelRegister", 3, null), onImagePanelRegister = prop($$props, "onImagePanelRegister", 3, null), onPosePanelRegister = prop($$props, "onPosePanelRegister", 3, null), onAIAssistantRegister = prop($$props, "onAIAssistantRegister", 3, null), onFooterRegister = prop($$props, "onFooterRegister", 3, null), getActivePoser = prop($$props, "getActivePoser", 3, () => null), apiURL = prop($$props, "apiURL", 3, (p) => p), fetchApi = prop($$props, "fetchApi", 3, (p, o) => fetch(p, o)), toast = prop($$props, "toast", 3, () => {
|
||||
}), docDropdownEl = prop($$props, "docDropdownEl", 3, null), modelIndicatorEl = prop($$props, "modelIndicatorEl", 3, null), tagsDropdownEl = prop($$props, "tagsDropdownEl", 3, null), getEditorView = prop($$props, "getEditorView", 3, () => null), onEditorPaneReady = prop($$props, "onEditorPaneReady", 3, null), onOutputPanelRegister = prop($$props, "onOutputPanelRegister", 3, null), onImagePanelRegister = prop($$props, "onImagePanelRegister", 3, null), onPosePanelRegister = prop($$props, "onPosePanelRegister", 3, null), onRegionPanelRegister = prop($$props, "onRegionPanelRegister", 3, null), onAIAssistantRegister = prop($$props, "onAIAssistantRegister", 3, null), onFooterRegister = prop($$props, "onFooterRegister", 3, null), getActivePoser = prop($$props, "getActivePoser", 3, () => null), getActiveRegionBox = prop($$props, "getActiveRegionBox", 3, () => null), apiURL = prop($$props, "apiURL", 3, (p) => p), fetchApi = prop($$props, "fetchApi", 3, (p, o) => fetch(p, o)), toast = prop($$props, "toast", 3, () => {
|
||||
}), getWorkflowId = prop($$props, "getWorkflowId", 3, () => ""), fetchWorkflowImages = prop($$props, "fetchWorkflowImages", 3, async () => []), fetchWorkflowCount = prop($$props, "fetchWorkflowCount", 3, async () => 0), subscribeHistory = prop($$props, "subscribeHistory", 3, () => () => {
|
||||
}), invalidateCache = prop($$props, "invalidateCache", 3, () => {
|
||||
}), openViewer = prop($$props, "openViewer", 3, () => {
|
||||
@@ -3499,6 +3760,7 @@ function NodeWidget($$anchor, $$props) {
|
||||
onToggleImage: () => onToggleImage()(),
|
||||
onToggleAssistant: () => onToggleAssistant()(),
|
||||
onTogglePose: () => onTogglePose()(),
|
||||
onToggleRegion: () => onToggleRegion()(),
|
||||
get onOpenFullscreen() {
|
||||
return onOpenFullscreen();
|
||||
},
|
||||
@@ -3527,7 +3789,25 @@ function NodeWidget($$anchor, $$props) {
|
||||
}
|
||||
});
|
||||
var node_3 = sibling(node_2, 2);
|
||||
AIAssistant(node_3, {
|
||||
RegionPanel(node_3, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
get shared() {
|
||||
return shared();
|
||||
},
|
||||
get getActiveRegionBox() {
|
||||
return getActiveRegionBox();
|
||||
},
|
||||
onToggle: (visible) => {
|
||||
shared().regionPanelVisible = visible;
|
||||
},
|
||||
get onRegister() {
|
||||
return onRegionPanelRegister();
|
||||
}
|
||||
});
|
||||
var node_4 = sibling(node_3, 2);
|
||||
AIAssistant(node_4, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
@@ -3541,10 +3821,10 @@ function NodeWidget($$anchor, $$props) {
|
||||
return onAIAssistantRegister();
|
||||
}
|
||||
});
|
||||
var div_2 = sibling(node_3, 2);
|
||||
var node_4 = child(div_2);
|
||||
var div_2 = sibling(node_4, 2);
|
||||
var node_5 = child(div_2);
|
||||
bind_this(
|
||||
EditorPane(node_4, {
|
||||
EditorPane(node_5, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
@@ -3555,8 +3835,8 @@ function NodeWidget($$anchor, $$props) {
|
||||
($$value) => editorPane = $$value,
|
||||
() => editorPane
|
||||
);
|
||||
var node_5 = sibling(node_4, 2);
|
||||
OutputPanel(node_5, {
|
||||
var node_6 = sibling(node_5, 2);
|
||||
OutputPanel(node_6, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
@@ -3570,8 +3850,8 @@ function NodeWidget($$anchor, $$props) {
|
||||
return onOutputPanelRegister();
|
||||
}
|
||||
});
|
||||
var node_6 = sibling(div_2, 2);
|
||||
ImagePanel(node_6, {
|
||||
var node_7 = sibling(div_2, 2);
|
||||
ImagePanel(node_7, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
@@ -3585,8 +3865,8 @@ function NodeWidget($$anchor, $$props) {
|
||||
return onImagePanelRegister();
|
||||
}
|
||||
});
|
||||
var node_7 = sibling(div_1, 2);
|
||||
Footer(node_7, {
|
||||
var node_8 = sibling(div_1, 2);
|
||||
Footer(node_8, {
|
||||
get node() {
|
||||
return $$props.node;
|
||||
},
|
||||
@@ -3632,7 +3912,9 @@ class SharedState {
|
||||
__privateAdd(this, _imagePanelVisible, state(false));
|
||||
__privateAdd(this, _aiAssistantOpen, state(false));
|
||||
__privateAdd(this, _posePanelVisible, state(false));
|
||||
__privateAdd(this, _regionPanelVisible, state(false));
|
||||
__privateAdd(this, _hasPoseStudio, state(false));
|
||||
__privateAdd(this, _hasRegionBox, state(false));
|
||||
__privateAdd(this, _compiledOutput, state(""));
|
||||
__privateAdd(this, _compiledNegOutput, state(""));
|
||||
__privateAdd(this, _imageUrl, state(null));
|
||||
@@ -3654,6 +3936,7 @@ class SharedState {
|
||||
this.imagePanelVisible = !!p.pcrImagePreview;
|
||||
this.aiAssistantOpen = !!p.pcrAiAssistant;
|
||||
this.posePanelVisible = !!p.pcrPosePanel;
|
||||
this.regionPanelVisible = !!p.pcrRegionPanel;
|
||||
this.compiledOutput = p.pcrCompiledOutput || "";
|
||||
this.compiledNegOutput = p.pcrCompiledNegOutput || "";
|
||||
}
|
||||
@@ -3753,12 +4036,24 @@ class SharedState {
|
||||
set posePanelVisible(value) {
|
||||
set(__privateGet(this, _posePanelVisible), value, true);
|
||||
}
|
||||
get regionPanelVisible() {
|
||||
return get(__privateGet(this, _regionPanelVisible));
|
||||
}
|
||||
set regionPanelVisible(value) {
|
||||
set(__privateGet(this, _regionPanelVisible), value, true);
|
||||
}
|
||||
get hasPoseStudio() {
|
||||
return get(__privateGet(this, _hasPoseStudio));
|
||||
}
|
||||
set hasPoseStudio(value) {
|
||||
set(__privateGet(this, _hasPoseStudio), value, true);
|
||||
}
|
||||
get hasRegionBox() {
|
||||
return get(__privateGet(this, _hasRegionBox));
|
||||
}
|
||||
set hasRegionBox(value) {
|
||||
set(__privateGet(this, _hasRegionBox), value, true);
|
||||
}
|
||||
get compiledOutput() {
|
||||
return get(__privateGet(this, _compiledOutput));
|
||||
}
|
||||
@@ -3818,7 +4113,9 @@ _outputPanelOpen = new WeakMap();
|
||||
_imagePanelVisible = new WeakMap();
|
||||
_aiAssistantOpen = new WeakMap();
|
||||
_posePanelVisible = new WeakMap();
|
||||
_regionPanelVisible = new WeakMap();
|
||||
_hasPoseStudio = new WeakMap();
|
||||
_hasRegionBox = new WeakMap();
|
||||
_compiledOutput = new WeakMap();
|
||||
_compiledNegOutput = new WeakMap();
|
||||
_imageUrl = new WeakMap();
|
||||
|
||||
@@ -433,6 +433,15 @@ function ItemThumbnail($$anchor, $$props) {
|
||||
if (thumbLoaded) set(showFlash, true);
|
||||
thumbLoaded = true;
|
||||
}
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
function handleDragOver(e) {
|
||||
if (item().type !== "folder") return;
|
||||
if (!e.dataTransfer.types.includes("application/x-promptchain-move")) return;
|
||||
@@ -464,8 +473,8 @@ function ItemThumbnail($$anchor, $$props) {
|
||||
{
|
||||
var consequent_1 = ($$anchor2) => {
|
||||
var fragment = root_1$9();
|
||||
var img = first_child(fragment);
|
||||
var node_1 = sibling(img, 2);
|
||||
var img_1 = first_child(fragment);
|
||||
var node_1 = sibling(img_1, 2);
|
||||
{
|
||||
var consequent = ($$anchor3) => {
|
||||
var div_2 = root_2$6();
|
||||
@@ -478,8 +487,8 @@ function ItemThumbnail($$anchor, $$props) {
|
||||
}
|
||||
template_effect(
|
||||
($0) => {
|
||||
set_attribute(img, "src", $0);
|
||||
set_attribute(img, "alt", item().name);
|
||||
set_attribute(img_1, "src", $0);
|
||||
set_attribute(img_1, "alt", item().name);
|
||||
},
|
||||
[
|
||||
() => apiURL(`/promptchain/browse/preview?scope=${$$props.scope}&path=${encodeURIComponent(item().path)}&thumb=1`)
|
||||
@@ -489,8 +498,8 @@ function ItemThumbnail($$anchor, $$props) {
|
||||
};
|
||||
var consequent_3 = ($$anchor2) => {
|
||||
var fragment_1 = root_3$7();
|
||||
var img_1 = first_child(fragment_1);
|
||||
var node_2 = sibling(img_1, 2);
|
||||
var img_2 = first_child(fragment_1);
|
||||
var node_2 = sibling(img_2, 2);
|
||||
{
|
||||
var consequent_2 = ($$anchor3) => {
|
||||
var div_3 = root_4$7();
|
||||
@@ -503,12 +512,13 @@ function ItemThumbnail($$anchor, $$props) {
|
||||
}
|
||||
template_effect(
|
||||
($0) => {
|
||||
set_attribute(img_1, "src", $0);
|
||||
set_attribute(img_1, "alt", item().name);
|
||||
set_attribute(img_2, "src", $0);
|
||||
set_attribute(img_2, "alt", item().name);
|
||||
},
|
||||
[() => apiURL(`/promptchain/thumb/${item().thumbnailHash}`)]
|
||||
);
|
||||
event("load", img_1, onThumbUpdate);
|
||||
event("load", img_2, onThumbUpdate);
|
||||
event("error", img_2, retryThumb);
|
||||
append($$anchor2, fragment_1);
|
||||
};
|
||||
var consequent_4 = ($$anchor2) => {
|
||||
@@ -722,6 +732,15 @@ function GridView($$anchor, $$props) {
|
||||
next.delete(itemPath);
|
||||
set(flashingThumbs, next, true);
|
||||
}
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
var div = root$2();
|
||||
var node = child(div);
|
||||
{
|
||||
@@ -790,6 +809,7 @@ function GridView($$anchor, $$props) {
|
||||
[() => thumbSrc(get(item))]
|
||||
);
|
||||
event("load", img_2, () => onThumbUpdate(get(item)));
|
||||
event("error", img_2, retryThumb);
|
||||
append($$anchor5, fragment_2);
|
||||
};
|
||||
var consequent_4 = ($$anchor5) => {
|
||||
@@ -1000,6 +1020,15 @@ function ListView($$anchor, $$props) {
|
||||
if (b < 1048576) return (b / 1024).toFixed(1) + " KB";
|
||||
return (b / 1048576).toFixed(1) + " MB";
|
||||
}
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
function fmtDate(ts) {
|
||||
if (!ts) return "-";
|
||||
return new Date(ts * 1e3).toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
||||
@@ -1059,22 +1088,23 @@ function ListView($$anchor, $$props) {
|
||||
var node_1 = child(span);
|
||||
{
|
||||
var consequent = ($$anchor3) => {
|
||||
var img = root_2$5();
|
||||
template_effect(($0) => set_attribute(img, "src", $0), [
|
||||
var img_1 = root_2$5();
|
||||
template_effect(($0) => set_attribute(img_1, "src", $0), [
|
||||
() => apiURL(`/promptchain/browse/preview?scope=${$$props.scope}&path=${encodeURIComponent(get(item).path)}&thumb=1`)
|
||||
]);
|
||||
append($$anchor3, img);
|
||||
append($$anchor3, img_1);
|
||||
};
|
||||
var consequent_1 = ($$anchor3) => {
|
||||
var svg = root_3$5();
|
||||
append($$anchor3, svg);
|
||||
};
|
||||
var consequent_2 = ($$anchor3) => {
|
||||
var img_1 = root_4$5();
|
||||
template_effect(($0) => set_attribute(img_1, "src", $0), [
|
||||
var img_2 = root_4$5();
|
||||
template_effect(($0) => set_attribute(img_2, "src", $0), [
|
||||
() => apiURL(`/promptchain/thumb/${get(item).thumbnailHash}`)
|
||||
]);
|
||||
append($$anchor3, img_1);
|
||||
event("error", img_2, retryThumb);
|
||||
append($$anchor3, img_2);
|
||||
};
|
||||
var consequent_3 = ($$anchor3) => {
|
||||
var svg_1 = root_5$5();
|
||||
|
||||
+2334
-952
File diff suppressed because it is too large
Load Diff
@@ -11,11 +11,148 @@ import { prepareInpaint, runInpaint, saveInpaintResult, uploadInputImage, inpain
|
||||
import { prepareEdit, saveEditedImage } from "./edit-image.js";
|
||||
import { fetchReposeCaps } from "./repose-from-image.js";
|
||||
import { runReposeInBackground } from "./repose-background.js";
|
||||
import { runExtendInBackground } from "./extend-background.js";
|
||||
import { runSmoothInBackground } from "./smooth-background.js";
|
||||
import { smoothNodesAvailable } from "./smooth-from-image.js";
|
||||
import { mountDetachedPoser } from "../pose-studio.js";
|
||||
import { showNodePackInstallModal } from "./model-bridge.js";
|
||||
import { loadCodeMirror, createEditor } from "./editor.js";
|
||||
import { activateTagAutocomplete } from "./autocomplete.js";
|
||||
|
||||
// "Extend video": which I2V template to continue with. A continuation is always
|
||||
// image->video, so it uses an I2V recipe regardless of how the source was made.
|
||||
// Pick the family whose models are installed (GGUF vs safetensors); the quant
|
||||
// resolver in buildExtendGraph then maps the template's baked variant to the
|
||||
// exact file on disk.
|
||||
async function pickI2VTemplate() {
|
||||
try {
|
||||
const res = await api.fetchApi("/promptchain/models/list");
|
||||
const models = res.ok ? ((await res.json())?.models || []) : [];
|
||||
const names = models.map((m) => (m.filename || "").toLowerCase());
|
||||
if (names.some((n) => n.endsWith(".gguf") && n.includes("i2v") && (n.includes("a14b") || n.includes("wan2.2")))) return "wan22-i2v-gguf";
|
||||
if (names.some((n) => /wan2\.2_i2v_.*(fp8_scaled|fp16)\.safetensors/.test(n))) return "wan22-i2v";
|
||||
} catch { /* fall through to default */ }
|
||||
return "wan22-i2v-gguf"; // the resolver remaps to whatever quant is actually installed
|
||||
}
|
||||
|
||||
async function fetchTemplateJson(id) {
|
||||
try {
|
||||
const res = await api.fetchApi(`/promptchain/templates/${encodeURIComponent(id)}`);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.template || data;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// Lightning (4-step) renders through the "-fast" template, which bakes the
|
||||
// LightX2V distill LoRAs. Picking it without those files on disk would build an
|
||||
// empty LoraLoaderModelOnly and render garbage with no error (the same failure
|
||||
// that rejected sf-video-test). Gate the option on the LoRAs actually being
|
||||
// installed. Fail-open on any probe error — server-side validation still
|
||||
// catches a genuinely missing file.
|
||||
async function lightningLorasInstalled(fastTemplate) {
|
||||
try {
|
||||
if (!fastTemplate?.nodes) return true;
|
||||
const infoRes = await api.fetchApi("/object_info/LoraLoaderModelOnly");
|
||||
if (!infoRes.ok) return true;
|
||||
const info = await infoRes.json();
|
||||
const installed = info?.LoraLoaderModelOnly?.input?.required?.lora_name?.[0] || [];
|
||||
const installedSet = new Set(installed.map((n) => String(n).toLowerCase()));
|
||||
const needed = fastTemplate.nodes
|
||||
.filter((n) => (n.type || n.comfyClass) === "LoraLoaderModelOnly")
|
||||
.map((n) => String(n.widgets_values?.[0] || "").toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (!needed.length) return true;
|
||||
return needed.every((f) => installedSet.has(f));
|
||||
} catch { return true; }
|
||||
}
|
||||
|
||||
// Per-recipe sampler defaults read from the SELECTED template's baked widgets —
|
||||
// NOT a config, because config presets only describe the normal recipe and are
|
||||
// wrong for the 4-step lightning one. KSamplerAdvanced widget order:
|
||||
// add_noise, noise_seed, control_after_generate, steps, cfg, sampler_name,
|
||||
// scheduler, start_at_step, end_at_step, return_with_leftover_noise.
|
||||
function extractRecipeDefaults(template) {
|
||||
if (!template?.nodes) return null;
|
||||
const find = (t) => template.nodes.find((n) => (n.type || n.comfyClass) === t);
|
||||
const all = (t) => template.nodes.filter((n) => (n.type || n.comfyClass) === t);
|
||||
const ks = all("KSamplerAdvanced");
|
||||
const high = ks.find((n) => (n.widgets_values || [])[0] === "enable") || ks[0];
|
||||
const hv = high?.widgets_values || [];
|
||||
const ms = find("ModelSamplingSD3");
|
||||
return {
|
||||
steps: Number(hv[3]) || 20,
|
||||
cfg: hv[4] != null ? Number(hv[4]) : 3.5,
|
||||
sampler: hv[5] || "euler",
|
||||
scheduler: hv[6] || "simple",
|
||||
shift: Number((ms?.widgets_values || [])[0]) || 5,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareExtend(hash) {
|
||||
if (!hash) return null;
|
||||
const res = await api.fetchApi(`/promptchain/extend/prepare?hash=${encodeURIComponent(hash)}`);
|
||||
if (!res.ok) return null;
|
||||
const d = await res.json();
|
||||
if (!d?.last_frame) return null;
|
||||
const q = new URLSearchParams({ filename: d.last_frame, subfolder: "", type: "input" });
|
||||
const templateId = await pickI2VTemplate();
|
||||
const [normalTpl, fastTpl] = await Promise.all([
|
||||
fetchTemplateJson(templateId),
|
||||
fetchTemplateJson(`${templateId}-fast`),
|
||||
]);
|
||||
return {
|
||||
lastFrameFilename: d.last_frame,
|
||||
lastFrameUrl: api.apiURL(`/view?${q}`),
|
||||
sourceRef: d.source_ref || "",
|
||||
fps: d.fps || 16,
|
||||
frameCount: d.frame_count || 0,
|
||||
width: d.width || 0,
|
||||
height: d.height || 0,
|
||||
templateId, // vanilla base; confirmExtend appends "-fast" for the Lightning recipe
|
||||
lightningAvailable: await lightningLorasInstalled(fastTpl),
|
||||
// Slider seeds: clip dims follow the SOURCE (drops the 640×640 hardcode).
|
||||
// Length defaults to the source's DURATION in native-16fps frames (snapped to
|
||||
// Wan's 4n+1) — so a smoothed 161f@32fps clip (5s) asks for ~81, not 161
|
||||
// (which would be a too-long 10s render). fps default is the i2v native (16).
|
||||
defaults: {
|
||||
// Snap dims to /16 (Wan's latent grid) and clamp to a trained ceiling, so a
|
||||
// foreign import's odd 1080p dims can't crash the patchify reshape.
|
||||
width: Math.min(1280, Math.round((d.width || 640) / 16) * 16),
|
||||
height: Math.min(1280, Math.round((d.height || 640) / 16) * 16),
|
||||
length: (() => {
|
||||
const dur = (d.frame_count || 81) / (d.fps || 16);
|
||||
const native = Math.round(dur * 16);
|
||||
// Ceiling 161 (~10s): length is ONE continuation clip, never the whole
|
||||
// (possibly long / already-extended) source duration — else it OOMs.
|
||||
return Math.min(161, Math.max(5, Math.round((native - 1) / 4) * 4 + 1));
|
||||
})(),
|
||||
fps: d.fps || 16,
|
||||
},
|
||||
recipeDefaults: {
|
||||
normal: extractRecipeDefaults(normalTpl),
|
||||
lightning: extractRecipeDefaults(fastTpl),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// "Smooth": offer to install the RIFE pack (Frame-Interpolation) on first use,
|
||||
// then re-register node defs so RIFE VFI becomes available without a full reload.
|
||||
function installFrameInterpPack() {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const settle = (v) => { if (!settled) { settled = true; resolve(v); } };
|
||||
showNodePackInstallModal(
|
||||
"FrameInterpolation",
|
||||
async () => {
|
||||
try { const defs = await app.getNodeDefs(); await app.registerNodesFromDefs(defs); } catch {}
|
||||
settle(true);
|
||||
},
|
||||
() => settle(false),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// The reference box must show the prompt that actually made THIS image.
|
||||
// The embedded prompt is authoritative whenever it still carries $region
|
||||
// blocks — those never survive grafted re-renders, so their presence means
|
||||
@@ -165,6 +302,12 @@ export async function openViewer(images, startIndex = 0, workflowId = "", onDele
|
||||
onReposeCaps: () => fetchReposeCaps(),
|
||||
onReposeRun: (opts) => runReposeInBackground(opts),
|
||||
onMountPoser: (el, opts) => mountDetachedPoser(el, opts),
|
||||
onExtendPrepare: (hash) => prepareExtend(hash),
|
||||
onExtendRun: (opts) => runExtendInBackground(opts),
|
||||
onSmoothCheck: () => smoothNodesAvailable(),
|
||||
onSmoothInstall: () => installFrameInterpPack(),
|
||||
onSmoothPrepare: (hash) => prepareExtend(hash), // reuse: stages the source video + fps
|
||||
onSmoothRun: (opts) => runSmoothInBackground(opts),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+37
-1
@@ -17,6 +17,7 @@ import { applyVueMinWidthAll } from "./lib/sizing.js";
|
||||
import { attachSlotClickHandlers, attachCanvasClickHandler } from "./lib/slot-dropdown.js";
|
||||
import { getConnectedInputs, getLinkInfo } from "./lib/slot-utils.js";
|
||||
import { poseRegistry } from "./lib/pose-registry.js";
|
||||
import { regionBoxRegistry } from "./lib/region-box-registry.js";
|
||||
import { updateInputLabels, updateParentLabels, checkWildcardConflict, cleanupOrderChain } from "./lib/order-chain.js";
|
||||
import { createHighlightExtension } from "./lib/active-label-highlight.js";
|
||||
import { createWildcardBadgeExtension } from "./lib/wildcard-badge.js";
|
||||
@@ -126,6 +127,7 @@ function syncShared(node, shared) {
|
||||
shared.outputPanelOpen = !!node.properties?.pcrOutputPanel;
|
||||
shared.imagePanelVisible = !!node.properties?.pcrImagePreview;
|
||||
shared.posePanelVisible = !!node.properties?.pcrPosePanel;
|
||||
shared.regionPanelVisible = !!node.properties?.pcrRegionPanel;
|
||||
}
|
||||
|
||||
// --- Image preview: execution tracking + graph traversal ---
|
||||
@@ -139,7 +141,12 @@ let _executingNodeId = null;
|
||||
let _executionWorkflowId = null;
|
||||
let _lastPreviewUrl = null;
|
||||
|
||||
const OUTPUT_NODE_TYPES = new Set(["SaveImage", "PreviewImage"]);
|
||||
// SaveVideo/SaveWEBM are terminal outputs too: they emit a PreviewVideo UI
|
||||
// payload shaped like images ({images:[{filename,subfolder,type}], animated})
|
||||
// so the executed handler treats them uniformly. Without them here the panel's
|
||||
// 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", "PromptChain_IdeogramSampler"]);
|
||||
const IMAGE_LATENT_TYPES = new Set(["IMAGE", "LATENT"]);
|
||||
const LOAD_IMAGE_TYPES = new Set(["LoadImage"]);
|
||||
@@ -1399,6 +1406,13 @@ app.registerExtension({
|
||||
}
|
||||
node._pcrPosePanel?.toggle();
|
||||
};
|
||||
node._pcrMenubar.toggleRegion = () => {
|
||||
if (node.properties?.pcrCollapsed) {
|
||||
handleCollapse(node, false, container);
|
||||
if (shared) syncShared(node, shared);
|
||||
}
|
||||
node._pcrRegionPanel?.toggle();
|
||||
};
|
||||
node._pcrMenubar.resetIterate = () => {
|
||||
if (!node.properties) node.properties = {};
|
||||
node.properties.pcrIterateIndex = 0;
|
||||
@@ -1469,6 +1483,8 @@ app.registerExtension({
|
||||
getCanvasScale: () => app.canvas?.ds?.scale ?? 1,
|
||||
// the Poser node the pose panel should dock (shared registry, last-interacted)
|
||||
getActivePoser: () => poseRegistry.getActive(),
|
||||
// the Region Box node the region panel should dock (shared registry)
|
||||
getActiveRegionBox: () => regionBoxRegistry.getActive(),
|
||||
// registration callbacks
|
||||
onEditorPaneReady: (editorContainer) => {
|
||||
editorContainer._pcrNode = node;
|
||||
@@ -1527,6 +1543,9 @@ app.registerExtension({
|
||||
onPosePanelRegister: (panelApi) => {
|
||||
node._pcrPosePanel = panelApi;
|
||||
},
|
||||
onRegionPanelRegister: (panelApi) => {
|
||||
node._pcrRegionPanel = panelApi;
|
||||
},
|
||||
onAIAssistantRegister: (panelApi) => {
|
||||
node._pcrAiAssistant = panelApi;
|
||||
},
|
||||
@@ -1598,6 +1617,13 @@ app.registerExtension({
|
||||
}
|
||||
node._pcrPosePanel?.toggle();
|
||||
},
|
||||
onToggleRegion: () => {
|
||||
if (node.properties?.pcrCollapsed) {
|
||||
handleCollapse(node, false, container);
|
||||
if (shared) syncShared(node, shared);
|
||||
}
|
||||
node._pcrRegionPanel?.toggle();
|
||||
},
|
||||
onOpenFullscreen: () => {
|
||||
import("./lib/fullscreen-bridge.js")
|
||||
.then(m => m.showSvelteFullscreen(node))
|
||||
@@ -1616,6 +1642,14 @@ app.registerExtension({
|
||||
updatePosePresence();
|
||||
node._pcrUnsubscribePose = poseRegistry.subscribe(updatePosePresence);
|
||||
|
||||
// Same gate for the Region Box panel — driven from its own registry.
|
||||
const updateRegionPresence = () => {
|
||||
if (shared) shared.hasRegionBox = regionBoxRegistry.count > 0;
|
||||
node._pcrRegionPanel?.refreshDock?.();
|
||||
};
|
||||
updateRegionPresence();
|
||||
node._pcrUnsubscribeRegion = regionBoxRegistry.subscribe(updateRegionPresence);
|
||||
|
||||
// Restore tag config now that node properties are available
|
||||
tagsDropdown.restore();
|
||||
|
||||
@@ -1698,6 +1732,8 @@ app.registerExtension({
|
||||
cleanupOrderChain(node.id);
|
||||
node._pcrUnsubscribePose?.();
|
||||
node._pcrUnsubscribePose = null;
|
||||
node._pcrUnsubscribeRegion?.();
|
||||
node._pcrUnsubscribeRegion = null;
|
||||
if (svelteInstance && svelteModule) {
|
||||
svelteModule.destroyNodeWidget(svelteInstance);
|
||||
}
|
||||
|
||||
+103
-17
@@ -10,6 +10,7 @@ import { api } from "../../scripts/api.js";
|
||||
import { poseRegistry } from "./lib/pose-registry.js";
|
||||
import { setEditorContent, attachDocChangeListener } from "./lib/editor.js";
|
||||
import { getLink } from "./lib/slot-utils.js";
|
||||
import { traceRegionConsumer, ensureConsumerPoseWire, seedRegionBlocks, readRegionBlockBodies } from "./lib/regional-binding.js";
|
||||
|
||||
const NODE_TYPE = "PromptChain_PoseStudio";
|
||||
const MIN_NODE_SIZE = [320, 400];
|
||||
@@ -21,11 +22,17 @@ console.log("%c[PromptChain PoseStudio] BUILD region-rename-reverse-1 (text $nam
|
||||
// ── figure/prop clipboard ───────────────────────────────────────────────────
|
||||
// Copy a selection (person, prop, or band-selected group) and paste it into the
|
||||
// same viewport or any other 3D Poser — including one in another workflow. The
|
||||
// payload rides the SYSTEM clipboard (survives workflow switches and browser
|
||||
// restarts); this module-level copy is the fallback when the clipboard API is
|
||||
// unavailable or permission-denied (then paste works within the current tab).
|
||||
// payload rides THREE channels, tried in order on paste: the SYSTEM clipboard
|
||||
// (crosses browsers/profiles, but its async read is permission- and focus-gated
|
||||
// and silently times out cross-tab), this module-level copy (same page only),
|
||||
// and localStorage (shared across every tab/window of this origin and surviving
|
||||
// reloads, with no permission gate — the reliable bridge between two workflows
|
||||
// opened in separate tabs, which the clipboard read can't be counted on for).
|
||||
const CLIP_FORMAT = "promptchain-pose-clip";
|
||||
const POSE_CLIP_STORE_KEY = "promptchain:poseClip";
|
||||
let poseClipMemory = null;
|
||||
function writePoseClipStore(json) { try { localStorage.setItem(POSE_CLIP_STORE_KEY, json); } catch {} }
|
||||
function readPoseClipStore() { try { return localStorage.getItem(POSE_CLIP_STORE_KEY); } catch { return null; } }
|
||||
|
||||
// ── lazy Three.js loader ────────────────────────────────────────────────────
|
||||
// three.bundle.js is an IIFE that assigns window.PromptChainThree, but only
|
||||
@@ -1081,6 +1088,25 @@ function ensureCouplePoseWire(node) {
|
||||
if (poseOut >= 0) node.connect(poseOut, couple, poseIn);
|
||||
}
|
||||
|
||||
// Inject region prompts carried from a cross-scene paste (a character built in
|
||||
// another workflow brings its $name{} body with it). Held on the node until the
|
||||
// bound PromptChain editor exists, so a paste made before that editor builds
|
||||
// isn't lost — re-tried from captureAndUpload on the next capture, never polled.
|
||||
// Uses the broad consumer trace (AttentionCouple / RegionalConditioning /
|
||||
// Ideogram), so "region mode" here is purely the destination's wiring.
|
||||
function flushPendingRegions(node) {
|
||||
const pend = node._pcrPendingRegions;
|
||||
if (!pend?.length) return;
|
||||
// Only seed entities that still exist — a figure deleted before the bound
|
||||
// editor became available must not leave a ghost $name{} bound to no mask row.
|
||||
const live = node._pcrPose?.entityNames?.();
|
||||
const fresh = live ? pend.filter((e) => live.some((n) => n.toLowerCase() === e.name.toLowerCase())) : pend;
|
||||
// seedRegionBlocks returns true when done OR when the dest isn't region-wired
|
||||
// at all (nothing to inject into) — either way stop retrying; only a wired
|
||||
// consumer whose editor isn't built yet keeps the paste pending.
|
||||
if (seedRegionBlocks(node, fresh)) node._pcrPendingRegions = null;
|
||||
}
|
||||
|
||||
// Content id for a capture set (control map + masks), salted with the node id.
|
||||
// The salt keeps two posers in one graph from sharing filenames — sharing would
|
||||
// let one poser's superseded-set delete orphan the other's live reference.
|
||||
@@ -1174,6 +1200,10 @@ async function captureAndUpload(node) {
|
||||
ps.uploadedOnce = true;
|
||||
window.dispatchEvent(new CustomEvent("promptchain:pose-uploaded", { detail: { nodeId: node.id } }));
|
||||
}
|
||||
// Body-aware first: drain any region prompts carried in from a cross-scene
|
||||
// paste (writes $name{ tags }), so the add-only reconcile below sees those
|
||||
// blocks already present and won't re-seed them empty.
|
||||
flushPendingRegions(node);
|
||||
// Regional: keep the bound PromptChain prompt's $name{} blocks in sync
|
||||
// with the region entities — figures AND named props (add-only). No-op
|
||||
// unless this Poser feeds an AttentionCouple.
|
||||
@@ -8434,19 +8464,27 @@ async function mountViewport(node, container) {
|
||||
}
|
||||
}
|
||||
const clip = serializeClip(sel);
|
||||
// Carry each selected NAMED entity's region prompt ($name{ body }) so a
|
||||
// character built here keeps its prompt when pasted into another scene.
|
||||
const selNames = sel.map((s) => (s.kind === "figure" ? s.o.customName : s.o.userData?.customName)).filter(Boolean);
|
||||
const regions = readRegionBlockBodies(node, selNames);
|
||||
if (Object.keys(regions).length) clip.regions = regions;
|
||||
const json = JSON.stringify(clip);
|
||||
poseClipMemory = json;
|
||||
writePoseClipStore(json); // cross-tab/-reload bridge, no clipboard permission needed
|
||||
let system = false;
|
||||
// Bounded: in some contexts the clipboard promise never settles (no
|
||||
// permission UI to resolve it) — cut/copy must not wedge behind it; the
|
||||
// tab-local fallback is already written above.
|
||||
// tab-local + localStorage fallbacks are already written above.
|
||||
try {
|
||||
await Promise.race([
|
||||
navigator.clipboard.writeText(json).then(() => { system = true; }),
|
||||
new Promise((res) => setTimeout(res, 1500)),
|
||||
]);
|
||||
} catch {} // http-non-localhost / permission denied → tab-local fallback
|
||||
showClipToast(`copied ${clipCountLabel(clip)}${system ? "" : " (this browser tab only)"}`);
|
||||
} catch {} // http-non-localhost / permission denied → localStorage bridge still carries it
|
||||
// System clipboard reaches other browsers/profiles; without it the
|
||||
// localStorage bridge still reaches every tab/window of THIS browser.
|
||||
showClipToast(`copied ${clipCountLabel(clip)}${system ? "" : " (this browser only)"}`);
|
||||
};
|
||||
const parseClip = (text) => {
|
||||
if (!text) return null;
|
||||
@@ -8463,11 +8501,22 @@ async function mountViewport(node, container) {
|
||||
new Promise((res) => setTimeout(() => res(null), 1500)), // never-settling read → tab-local fallback
|
||||
]);
|
||||
} catch {}
|
||||
const clip = parseClip(sysText) || parseClip(poseClipMemory);
|
||||
const clip = parseClip(sysText) || parseClip(poseClipMemory) || parseClip(readPoseClipStore());
|
||||
if (!clip) { showClipToast("nothing to paste — copy a person or prop first (right-click → copy)", true); return; }
|
||||
if (!node._pcrAlive) return;
|
||||
const data = JSON.parse(JSON.stringify(clip)); // position nudges below must not mutate the stored clipboard
|
||||
|
||||
// groupId is per-node (groupIdSeq starts at 1 in every Poser), so a pasted
|
||||
// "g1" would silently fuse with an unrelated "g1" already in this scene.
|
||||
// Remap each distinct clip group to a fresh destination-unique id before
|
||||
// rebuild — buildPropFrom then restores the already-fresh id verbatim.
|
||||
const groupRemap = new Map();
|
||||
for (const p of data.props) {
|
||||
if (typeof p.groupId !== "string" || !p.groupId) continue;
|
||||
if (!groupRemap.has(p.groupId)) groupRemap.set(p.groupId, "g" + (groupIdSeq++));
|
||||
p.groupId = groupRemap.get(p.groupId);
|
||||
}
|
||||
|
||||
// Nudge the whole group sideways onto free ground — pasting in place would
|
||||
// stack the duplicate exactly inside the original. Relative arrangement is
|
||||
// preserved (one shared dx); a paste into open space (other workflow) lands
|
||||
@@ -8478,17 +8527,45 @@ async function mountViewport(node, container) {
|
||||
const spots = [...data.figures.map((fd) => fd.root || [0, 0, 0]), ...data.props.map((p) => p.pos || [0, 0, 0])];
|
||||
let dx = 0;
|
||||
while (dx < 6 && spots.some(([x, , z]) => taken(x + dx, z))) dx += 0.5;
|
||||
// Custom names must stay unique across ALL region entities (figures + named
|
||||
// props): a colliding pasted name (e.g. duplicating "alice" or "sword" in
|
||||
// the same scene) falls back to default/unnamed — two masks under one name
|
||||
// would bind ambiguously.
|
||||
// A custom name already live in this scene belongs to a DIFFERENT character
|
||||
// (two people independently built as "maid1" must stay distinct), so suffix
|
||||
// the paste to the next free "<name>_<n>" instead of dropping it — the
|
||||
// region and its carried prompt travel intact. Stays within
|
||||
// validateEntityName's grammar (letters/digits/_, starts with a letter;
|
||||
// never "mannequinN", which only auto-default figures use — and a default
|
||||
// figure carries no clip name, so collisions here are always custom↔custom).
|
||||
const usedNames = new Set(liveEntityNames().map((n) => n.toLowerCase()));
|
||||
const droppedNames = [];
|
||||
const clipRegions = data.regions || {}; // { sourceName: bodyText }
|
||||
const pastedRegions = []; // { name: finalName, body } to inject into the bound prompt
|
||||
const renamedNames = []; // "old → new" notes for the toast
|
||||
const carriedBodies = new Set(); // source names whose body is already claimed
|
||||
const resolvePastedName = (raw) => {
|
||||
// Defensive: a clip name should already be grammar-valid, but never let an
|
||||
// odd one through to become a $scope. null → treat as unnamed/default.
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw) || /^mannequin\d+$/i.test(raw)) return null;
|
||||
let nm = raw;
|
||||
if (usedNames.has(nm.toLowerCase())) {
|
||||
let n = 2;
|
||||
while (usedNames.has((raw + "_" + n).toLowerCase())) n++;
|
||||
nm = raw + "_" + n;
|
||||
renamedNames.push(raw + " → " + nm);
|
||||
}
|
||||
usedNames.add(nm.toLowerCase());
|
||||
// Carry the source body once per source name — figures are processed before
|
||||
// props, so the entity that actually owns the $block claims it; a second
|
||||
// entity sharing the raw name (only via a malformed clip) gets no body.
|
||||
// typeof guard: a region literally named "__proto__"/"toString" must read as
|
||||
// empty, not as an inherited Object.prototype member.
|
||||
const carriedBody = !carriedBodies.has(raw) && typeof clipRegions[raw] === "string" ? clipRegions[raw] : "";
|
||||
carriedBodies.add(raw);
|
||||
pastedRegions.push({ name: nm, body: carriedBody });
|
||||
return nm;
|
||||
};
|
||||
for (const fd of data.figures) {
|
||||
delete fd.uid; // pasted person gets a fresh identity — never collide with the source's
|
||||
if (typeof fd.name === "string" && fd.name) {
|
||||
if (usedNames.has(fd.name.toLowerCase())) { droppedNames.push(fd.name); delete fd.name; }
|
||||
else usedNames.add(fd.name.toLowerCase());
|
||||
const nm = resolvePastedName(fd.name);
|
||||
if (nm) fd.name = nm; else delete fd.name;
|
||||
}
|
||||
if (Array.isArray(fd.root)) fd.root[0] += dx;
|
||||
if (fd.footPins) for (const k of Object.keys(fd.footPins)) fd.footPins[k][0] += dx; // world anchors ride along
|
||||
@@ -8496,8 +8573,8 @@ async function mountViewport(node, container) {
|
||||
for (const p of data.props) {
|
||||
delete p.uid; // fresh identity, same rule as figures
|
||||
if (typeof p.name === "string" && p.name) {
|
||||
if (usedNames.has(p.name.toLowerCase())) { droppedNames.push(p.name); delete p.name; }
|
||||
else usedNames.add(p.name.toLowerCase());
|
||||
const nm = resolvePastedName(p.name);
|
||||
if (nm) p.name = nm; else delete p.name;
|
||||
}
|
||||
if (Array.isArray(p.pos)) p.pos[0] += dx;
|
||||
}
|
||||
@@ -8541,8 +8618,17 @@ async function mountViewport(node, container) {
|
||||
]);
|
||||
updatePinBtn();
|
||||
syncBodySliders();
|
||||
// Carry each pasted character's region + prompt into the bound prompt: stash
|
||||
// the bodies and inject now (flushed again on the next capture if the bound
|
||||
// editor isn't built yet), and ensure the POSE_JSON→consumer wire so the
|
||||
// server can bind each $name to its mask row. No-op when not region-wired.
|
||||
if (pastedRegions.length) {
|
||||
node._pcrPendingRegions = (node._pcrPendingRegions || []).concat(pastedRegions);
|
||||
flushPendingRegions(node);
|
||||
ensureConsumerPoseWire(node);
|
||||
}
|
||||
showClipToast(`pasted ${clipCountLabel(data)}` +
|
||||
(droppedNames.length ? ` — name${droppedNames.length > 1 ? "s" : ""} ${droppedNames.map((n) => `"${n}"`).join(", ")} taken → default` : ""));
|
||||
(renamedNames.length ? ` — renamed ${renamedNames.map((n) => `"${n}"`).join(", ")} (already in scene)` : ""));
|
||||
requestRender();
|
||||
captureAndUpload(node);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,983 @@
|
||||
// Region Box — a flat rectangle canvas + region list mounted into the
|
||||
// PromptChain_RegionBox node body. You draw, name, recolor, reorder and delete
|
||||
// rectangles on the output frame; the backend node rasterizes each into a region
|
||||
// mask. Wire MASKS + POSE_JSON into a PromptChain_AttentionCouple (SDXL) or
|
||||
// PromptChain_RegionalConditioning (Flux) or PromptChain_IdeogramCaption exactly
|
||||
// like the 3D Poser, and each `$name{}` block in the prompt paints only inside
|
||||
// its box.
|
||||
//
|
||||
// Like the 3D Poser, the canvas is a single relocatable element that can be
|
||||
// "docked" into a roomy left-side panel (RegionPanel.svelte) — see enterDock /
|
||||
// exitDock + region-box-registry.js. A SEPARATE extension from the Poser so a JS
|
||||
// error here can never take down the Poser or the prompt editor. The region-name
|
||||
// binding lives in the shared ./lib/regional-binding.js (which covers BOTH
|
||||
// consumer classes).
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { getLink } from "./lib/slot-utils.js";
|
||||
import { regionBoxRegistry } from "./lib/region-box-registry.js";
|
||||
import {
|
||||
traceRegionConsumer,
|
||||
reconcileRegionBlocks,
|
||||
renameRegionBlock,
|
||||
ensureConsumerPoseWire,
|
||||
} from "./lib/regional-binding.js";
|
||||
|
||||
const NODE_TYPE = "PromptChain_RegionBox";
|
||||
const MIN_NODE_SIZE = [340, 360];
|
||||
const MIN_BOX = 0.02; // ignore accidental micro-drags (fraction of frame)
|
||||
const HANDLE_VIS = 4; // half-size of a drawn resize handle (px)
|
||||
const HANDLE_HIT = 12; // px radius to grab a resize handle
|
||||
const DEADZONE = 4; // px a click must travel before it becomes a move
|
||||
const SIDEBAR_SHOW_WIDTH = 380; // container width at/above which the region list shows
|
||||
const PALETTE = ["#5b9dff", "#ff7a7a", "#67d98b", "#f0c651", "#c08bff", "#54d6d6", "#ff9f5b", "#e06fb0"];
|
||||
|
||||
console.log("%c[PromptChain RegionBox] BUILD region-box-2 (panel + list redesign) loaded", "color:#7fffb0;font-weight:bold");
|
||||
|
||||
let idSeq = 1;
|
||||
function genId() { return "rb" + (idSeq++); }
|
||||
|
||||
// ── tiny widget helpers (local copies; trivial, not the shared binding logic) ──
|
||||
function setWidgetValue(node, name, value) {
|
||||
const w = node.widgets?.find((x) => x.name === name);
|
||||
if (w) w.value = value;
|
||||
}
|
||||
function widgetValue(node, name) {
|
||||
return node.widgets?.find((x) => x.name === name)?.value;
|
||||
}
|
||||
function hideWidget(node, name) {
|
||||
const w = node.widgets?.find((x) => x.name === name);
|
||||
if (!w) return;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, -4];
|
||||
}
|
||||
|
||||
// ── one-time stylesheet for the region list (hover states need real CSS) ──────
|
||||
function installRegionStyles() {
|
||||
if (document.getElementById("pcr-region-box-styles")) return;
|
||||
const s = document.createElement("style");
|
||||
s.id = "pcr-region-box-styles";
|
||||
s.textContent = `
|
||||
.pcr-region-box { display:flex; flex-direction:row; }
|
||||
.pcr-rb-sidebar {
|
||||
display:flex; flex-direction:column; width:150px; flex-shrink:0;
|
||||
background:#15151b; border-right:1px solid #2c2c36; min-width:0; overflow:hidden;
|
||||
}
|
||||
.pcr-rb-head {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:6px 8px; font:600 11px/1 sans-serif; color:#9aa0ad;
|
||||
letter-spacing:.04em; text-transform:uppercase; border-bottom:1px solid #2c2c36;
|
||||
}
|
||||
.pcr-rb-addbtn {
|
||||
background:#2a2f3a; color:#cfe3ff; border:1px solid #3a4150; border-radius:4px;
|
||||
font:600 11px sans-serif; padding:2px 7px; cursor:pointer; white-space:nowrap;
|
||||
}
|
||||
.pcr-rb-addbtn:hover { background:#34506e; border-color:#4a6a90; }
|
||||
.pcr-rb-list { flex:1; overflow-y:auto; overflow-x:hidden; padding:4px; }
|
||||
.pcr-rb-empty { color:#6a6a78; font:italic 11px/1.4 sans-serif; padding:10px 8px; text-align:center; }
|
||||
.pcr-rb-row {
|
||||
display:flex; align-items:center; gap:5px; padding:4px 5px; border-radius:4px;
|
||||
cursor:pointer; margin-bottom:2px;
|
||||
}
|
||||
.pcr-rb-row:hover { background:#20212a; }
|
||||
.pcr-rb-row.active { background:#2c3242; outline:1px solid #4a6a90; }
|
||||
.pcr-rb-swatch {
|
||||
width:13px; height:13px; border-radius:3px; flex-shrink:0; cursor:pointer;
|
||||
border:1px solid rgba(255,255,255,.25);
|
||||
}
|
||||
.pcr-rb-name {
|
||||
flex:1; min-width:0; color:#dfe3ea; font:12px sans-serif;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||
}
|
||||
.pcr-rb-nameinput {
|
||||
flex:1; min-width:0; background:#1a1a22; color:#cfe3ff;
|
||||
border:1px solid #5b9dff; border-radius:3px; padding:1px 4px; font:12px sans-serif; outline:none;
|
||||
}
|
||||
.pcr-rb-iconbtn {
|
||||
background:transparent; border:none; color:#7c8190; cursor:pointer;
|
||||
font:12px/1 sans-serif; padding:1px 3px; border-radius:3px; flex-shrink:0;
|
||||
}
|
||||
.pcr-rb-iconbtn:hover { color:#fff; background:#3a4150; }
|
||||
.pcr-rb-iconbtn:disabled { opacity:.25; cursor:default; background:transparent; }
|
||||
.pcr-rb-canvas-wrap { position:relative; flex:1; min-width:0; }
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
// ── latent-resolution awareness (fork of the Poser's, scoped to RegionBox) ────
|
||||
// The canvas letterboxes to the generation aspect, so the box node mirrors the
|
||||
// downstream latent's width/height. Forked rather than reused because the
|
||||
// Poser's helpers self-skip on its own node type and re-sync via the pose
|
||||
// registry — both must point at RegionBox here instead.
|
||||
const regionBoxNodes = new Set();
|
||||
|
||||
const SAMPLER_TYPES = new Set([
|
||||
"KSampler", "KSamplerAdvanced", "SamplerCustom", "SamplerCustomAdvanced",
|
||||
"PromptChain_RegionalDetailer", "UltimateSDUpscale",
|
||||
]);
|
||||
const LATENT_RES_TYPES = new Set([
|
||||
"EmptyLatentImage", "EmptySD3LatentImage", "EmptyHunyuanLatentVideo",
|
||||
"SDXLEmptyLatentSizePicker", "EmptyImage", "EmptyMochiLatentVideo", "EmptyLTXVLatentVideo",
|
||||
]);
|
||||
|
||||
function findDownstreamSampler(start, graph) {
|
||||
const seen = new Set();
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const n = queue.shift();
|
||||
if (seen.has(n.id)) continue;
|
||||
seen.add(n.id);
|
||||
for (const out of n.outputs || []) {
|
||||
for (const lid of out.links || []) {
|
||||
const l = getLink(graph, lid);
|
||||
if (!l) continue;
|
||||
const t = graph.getNodeById(l.target_id);
|
||||
if (!t) continue;
|
||||
if (SAMPLER_TYPES.has(t.comfyClass || t.type)) return t;
|
||||
queue.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findResUpstream(start, graph) {
|
||||
const seen = new Set();
|
||||
const queue = [start];
|
||||
let fallback = null;
|
||||
while (queue.length) {
|
||||
const n = queue.shift();
|
||||
if (seen.has(n.id)) continue;
|
||||
seen.add(n.id);
|
||||
// Skip our OWN node type so a box never adopts its own width/height as the
|
||||
// latent source (the Poser's copy skips PromptChain_PoseStudio here).
|
||||
if ((n.comfyClass || n.type) !== NODE_TYPE) {
|
||||
const w = n.widgets?.find((x) => x.name === "width");
|
||||
const h = n.widgets?.find((x) => x.name === "height");
|
||||
if (w && h && typeof w.value === "number" && typeof h.value === "number") {
|
||||
const res = { width: Math.round(w.value), height: Math.round(h.value), node: n };
|
||||
if (LATENT_RES_TYPES.has(n.comfyClass || n.type)) return res;
|
||||
if (!fallback) fallback = res;
|
||||
}
|
||||
}
|
||||
for (const inp of n.inputs || []) {
|
||||
if (inp.link == null) continue;
|
||||
const l = getLink(graph, inp.link);
|
||||
const s = l && graph.getNodeById(l.origin_id);
|
||||
if (s) queue.push(s);
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function findLatentResolution(node) {
|
||||
const graph = app.graph;
|
||||
if (!graph) return null;
|
||||
const sampler = findDownstreamSampler(node, graph);
|
||||
if (!sampler) return null;
|
||||
const latentInput = (sampler.inputs || []).find((i) => /latent|samples/i.test(i.name) && i.link != null);
|
||||
let start = sampler;
|
||||
if (latentInput) {
|
||||
const l = getLink(graph, latentInput.link);
|
||||
const src = l && graph.getNodeById(l.origin_id);
|
||||
if (src) start = src;
|
||||
}
|
||||
return findResUpstream(start, graph);
|
||||
}
|
||||
|
||||
// Editing the latent's width/height pushes the new resolution into every live
|
||||
// box node — event-driven, no polling. Distinct hook flag from the Poser's so
|
||||
// both can hook the same latent node independently.
|
||||
function hookLatentResWidgets(resNode) {
|
||||
for (const name of ["width", "height"]) {
|
||||
const w = resNode.widgets?.find((x) => x.name === name);
|
||||
if (!w || w._pcrRegionResHooked) continue;
|
||||
w._pcrRegionResHooked = true;
|
||||
const orig = w.callback;
|
||||
w.callback = function (...args) {
|
||||
const r = orig?.apply(this, args);
|
||||
requestAnimationFrame(() => {
|
||||
for (const n of regionBoxNodes) if (n._pcrAlive) syncToLatent(n);
|
||||
});
|
||||
return r;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function syncToLatent(node) {
|
||||
const res = findLatentResolution(node);
|
||||
if (!res) return;
|
||||
if (res.node) hookLatentResWidgets(res.node);
|
||||
const wW = node.widgets?.find((w) => w.name === "width");
|
||||
const hW = node.widgets?.find((w) => w.name === "height");
|
||||
if (!wW || !hW) return;
|
||||
if (wW.value === res.width && hW.value === res.height) return;
|
||||
wW.value = res.width;
|
||||
hW.value = res.height;
|
||||
node._pcrRedraw?.(); // re-letterbox to the new aspect
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
// ── canvas ────────────────────────────────────────────────────────────────
|
||||
function setupRegionBox(node) {
|
||||
node._pcrAlive = true;
|
||||
installRegionStyles();
|
||||
regionBoxNodes.add(node);
|
||||
hideWidget(node, "box_state");
|
||||
|
||||
let boxes = []; // [{id,name,x,y,w,h,color?}] normalized 0-1, top-left origin
|
||||
let activeId = null; // id of the selected box, or null
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.className = "pcr-region-box";
|
||||
container.tabIndex = 0; // focusable so Delete only fires when the canvas has focus
|
||||
container.style.cssText =
|
||||
"position:relative;width:100%;height:100%;min-height:120px;outline:none;" +
|
||||
"background:#0d0d11;border-radius:6px;overflow:hidden;touch-action:none;";
|
||||
|
||||
// Region list sidebar (shown only when the surface is wide enough).
|
||||
const sidebar = document.createElement("div");
|
||||
sidebar.className = "pcr-rb-sidebar";
|
||||
const head = document.createElement("div");
|
||||
head.className = "pcr-rb-head";
|
||||
const headTitle = document.createElement("span");
|
||||
headTitle.textContent = "Regions";
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.className = "pcr-rb-addbtn";
|
||||
addBtn.textContent = "+ Add";
|
||||
addBtn.title = "Add a region in the centre of the frame";
|
||||
head.appendChild(headTitle);
|
||||
head.appendChild(addBtn);
|
||||
const syncBtn = document.createElement("button");
|
||||
syncBtn.className = "pcr-rb-addbtn";
|
||||
syncBtn.textContent = "⟲";
|
||||
syncBtn.title = "Pull $name{} regions from the prompt into boxes";
|
||||
syncBtn.addEventListener("click", (e) => { e.stopPropagation(); syncFromPrompt(); });
|
||||
head.appendChild(syncBtn);
|
||||
const list = document.createElement("div");
|
||||
list.className = "pcr-rb-list";
|
||||
sidebar.appendChild(head);
|
||||
sidebar.appendChild(list);
|
||||
container.appendChild(sidebar);
|
||||
|
||||
const canvasWrap = document.createElement("div");
|
||||
canvasWrap.className = "pcr-rb-canvas-wrap";
|
||||
canvasWrap.style.cursor = "crosshair";
|
||||
container.appendChild(canvasWrap);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
||||
canvasWrap.appendChild(canvas);
|
||||
|
||||
const hint = document.createElement("div");
|
||||
hint.textContent = "drag empty frame to add · drag a box or its $label to move · right-click to rename / delete";
|
||||
hint.style.cssText =
|
||||
"position:absolute;left:8px;bottom:6px;right:8px;pointer-events:none;z-index:2;" +
|
||||
"font:11px/1.3 sans-serif;color:#8a8a98;text-shadow:0 1px 2px #000;";
|
||||
canvasWrap.appendChild(hint);
|
||||
|
||||
node.addDOMWidget("region_box_view", "region_box_view", container, { serialize: false });
|
||||
|
||||
// Track this Region Box for the panel: register existence (gates the menubar
|
||||
// button) + mark last-touched so the panel docks whichever node you last used.
|
||||
regionBoxRegistry.add(node);
|
||||
// Capture phase so it fires even though the canvas's own onDown stops the
|
||||
// event from bubbling back up to the container.
|
||||
container.addEventListener("pointerdown", () => regionBoxRegistry.touch(node), true);
|
||||
// Keep sidebar/list interactions from reaching LiteGraph (node drag/select).
|
||||
// The canvas isolates itself in onDown; this covers the list controls. Bubble
|
||||
// phase so inner handlers still run first.
|
||||
for (const ev of ["pointerdown", "click", "dblclick", "wheel"]) {
|
||||
container.addEventListener(ev, (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
// Inline rename input on the canvas. window.prompt() is disabled in the
|
||||
// ComfyUI desktop (Electron) build — it silently returns null — so a prompt-
|
||||
// based rename never commits. Overlay <input> like the 3D Poser instead.
|
||||
const renameInput = document.createElement("input");
|
||||
renameInput.type = "text";
|
||||
renameInput.maxLength = 24;
|
||||
renameInput.spellcheck = false;
|
||||
renameInput.style.cssText =
|
||||
"position:absolute;z-index:7;display:none;background:#1a1a22;color:#cfe3ff;" +
|
||||
"border:1px solid #5b9dff;border-radius:4px;padding:2px 6px;" +
|
||||
"font:12px sans-serif;outline:none;";
|
||||
renameInput.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
canvasWrap.appendChild(renameInput);
|
||||
let renameId = null;
|
||||
let listRenameCommit = null; // commit fn for an in-place region-list rename, if open
|
||||
const rowEls = new Map(); // id -> { row, nameEl } so a rename can re-find its live row
|
||||
const closeRename = () => { renameId = null; renameInput.style.display = "none"; };
|
||||
const commitCanvasRename = () => {
|
||||
const id = renameId;
|
||||
if (id == null) return;
|
||||
const value = renameInput.value;
|
||||
closeRename();
|
||||
applyRename(id, value);
|
||||
};
|
||||
renameInput.addEventListener("keydown", (e) => {
|
||||
e.stopPropagation(); // graph hotkeys must not fire while typing
|
||||
if (e.key === "Enter") commitCanvasRename();
|
||||
else if (e.key === "Escape") closeRename();
|
||||
});
|
||||
renameInput.addEventListener("blur", commitCanvasRename);
|
||||
|
||||
// ── helpers ──
|
||||
function sanitize(name) {
|
||||
return String(name || "").replace(/[^\w]/g, "");
|
||||
}
|
||||
function uniqueName() {
|
||||
let n = 1;
|
||||
const have = new Set(boxes.map((b) => b.name));
|
||||
while (have.has("region" + n)) n++;
|
||||
return "region" + n;
|
||||
}
|
||||
function indexOfId(id) { return boxes.findIndex((b) => b.id === id); }
|
||||
function activeIndex() { return indexOfId(activeId); }
|
||||
function boxColor(b, i) { return b.color || PALETTE[i % PALETTE.length]; }
|
||||
|
||||
// letterbox the output aspect inside the canvas's own CSS pixel box
|
||||
function frame() {
|
||||
const cw = canvas.clientWidth || 1;
|
||||
const ch = canvas.clientHeight || 1;
|
||||
const ow = Math.max(1, widgetValue(node, "width") || 832);
|
||||
const oh = Math.max(1, widgetValue(node, "height") || 1216);
|
||||
const ar = ow / oh, car = cw / ch;
|
||||
let fw, fh;
|
||||
if (car > ar) { fh = ch; fw = ch * ar; } else { fw = cw; fh = cw / ar; }
|
||||
return { fx: (cw - fw) / 2, fy: (ch - fh) / 2, fw, fh, cw, ch };
|
||||
}
|
||||
|
||||
const toPx = (b, f) => ({ x: f.fx + b.x * f.fw, y: f.fy + b.y * f.fh, w: b.w * f.fw, h: b.h * f.fh });
|
||||
|
||||
// The drawn `$name` chip above a box — kept identical to draw() so it's a
|
||||
// grab/select/rename target too (clicking the label === clicking the box).
|
||||
function labelRect(b, f) {
|
||||
const r = toPx(b, f);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = "12px sans-serif";
|
||||
const tw = ctx.measureText("$" + (b.name || "region")).width;
|
||||
return { x: r.x, y: Math.max(f.fy, r.y - 16), w: tw + 8, h: 16 };
|
||||
}
|
||||
|
||||
// 8 resize handles for a px rect, keyed by which edges they move
|
||||
function handles(r) {
|
||||
return [
|
||||
{ id: "nw", x: r.x, y: r.y }, { id: "n", x: r.x + r.w / 2, y: r.y }, { id: "ne", x: r.x + r.w, y: r.y },
|
||||
{ id: "e", x: r.x + r.w, y: r.y + r.h / 2 }, { id: "se", x: r.x + r.w, y: r.y + r.h },
|
||||
{ id: "s", x: r.x + r.w / 2, y: r.y + r.h }, { id: "sw", x: r.x, y: r.y + r.h }, { id: "w", x: r.x, y: r.y + r.h / 2 },
|
||||
];
|
||||
}
|
||||
function cursorForHandle(id) {
|
||||
if (id === "nw" || id === "se") return "nwse-resize";
|
||||
if (id === "ne" || id === "sw") return "nesw-resize";
|
||||
if (id === "n" || id === "s") return "ns-resize";
|
||||
return "ew-resize";
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const f = frame();
|
||||
if (canvas.width !== Math.round(f.cw * dpr) || canvas.height !== Math.round(f.ch * dpr)) {
|
||||
canvas.width = Math.round(f.cw * dpr);
|
||||
canvas.height = Math.round(f.ch * dpr);
|
||||
}
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, f.cw, f.ch);
|
||||
|
||||
// dead (non-drawable) area vs the drawable "paper" frame — clear figure/ground
|
||||
ctx.fillStyle = "#0d0d11";
|
||||
ctx.fillRect(0, 0, f.cw, f.ch);
|
||||
ctx.fillStyle = "#1b1b22";
|
||||
ctx.fillRect(f.fx, f.fy, f.fw, f.fh);
|
||||
|
||||
// rule-of-thirds guides (subtle composition aid)
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let k = 1; k <= 2; k++) {
|
||||
const gx = Math.round(f.fx + (f.fw * k) / 3) + 0.5;
|
||||
const gy = Math.round(f.fy + (f.fh * k) / 3) + 0.5;
|
||||
ctx.beginPath(); ctx.moveTo(gx, f.fy); ctx.lineTo(gx, f.fy + f.fh); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(f.fx, gy); ctx.lineTo(f.fx + f.fw, gy); ctx.stroke();
|
||||
}
|
||||
|
||||
// bright drawable-frame border
|
||||
ctx.strokeStyle = "#5a5a6e";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(f.fx + 1, f.fy + 1, f.fw - 2, f.fh - 2);
|
||||
|
||||
const aIdx = activeIndex();
|
||||
boxes.forEach((b, i) => {
|
||||
const r = toPx(b, f);
|
||||
const color = boxColor(b, i);
|
||||
ctx.fillStyle = color + "2b"; // ~17% alpha fill
|
||||
ctx.fillRect(r.x, r.y, r.w, r.h);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = i === aIdx ? 2.5 : 1.5;
|
||||
ctx.strokeRect(r.x, r.y, r.w, r.h);
|
||||
|
||||
const label = "$" + (b.name || "region");
|
||||
ctx.font = "12px sans-serif";
|
||||
const tw = ctx.measureText(label).width;
|
||||
const ly = Math.max(f.fy, r.y - 16);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(r.x, ly, tw + 8, 16);
|
||||
ctx.fillStyle = "#101015";
|
||||
ctx.fillText(label, r.x + 4, ly + 12);
|
||||
|
||||
if (i === aIdx) {
|
||||
for (const hd of handles(r)) {
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(hd.x - HANDLE_VIS, hd.y - HANDLE_VIS, HANDLE_VIS * 2, HANDLE_VIS * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(hd.x - HANDLE_VIS + 1, hd.y - HANDLE_VIS + 1, HANDLE_VIS * 2 - 2, HANDLE_VIS * 2 - 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
node._pcrRedraw = draw;
|
||||
|
||||
// ── region list ──
|
||||
// Commit any open in-place rename before its row gets torn out / replaced.
|
||||
// Null the handle first so the commit's own renderList doesn't recurse here.
|
||||
function commitListRename() {
|
||||
if (!listRenameCommit) return;
|
||||
const c = listRenameCommit;
|
||||
listRenameCommit = null;
|
||||
c();
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
commitListRename();
|
||||
rowEls.clear();
|
||||
list.replaceChildren();
|
||||
if (!boxes.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "pcr-rb-empty";
|
||||
empty.textContent = "No regions yet — drag on the canvas or click + Add.";
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
boxes.forEach((b, i) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "pcr-rb-row" + (b.id === activeId ? " active" : "");
|
||||
|
||||
const swatch = document.createElement("div");
|
||||
swatch.className = "pcr-rb-swatch";
|
||||
swatch.style.background = boxColor(b, i);
|
||||
swatch.title = "Click to change colour";
|
||||
swatch.addEventListener("click", (e) => { e.stopPropagation(); recolor(b.id); });
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = "pcr-rb-name";
|
||||
name.textContent = "$" + (b.name || "region");
|
||||
name.title = "Double-click to rename";
|
||||
name.addEventListener("dblclick", (e) => { e.stopPropagation(); startListRename(b.id); });
|
||||
|
||||
const up = document.createElement("button");
|
||||
up.className = "pcr-rb-iconbtn"; up.textContent = "▲"; up.title = "Move back";
|
||||
up.disabled = i === 0;
|
||||
up.addEventListener("click", (e) => { e.stopPropagation(); reorder(b.id, -1); });
|
||||
|
||||
const down = document.createElement("button");
|
||||
down.className = "pcr-rb-iconbtn"; down.textContent = "▼"; down.title = "Move forward";
|
||||
down.disabled = i === boxes.length - 1;
|
||||
down.addEventListener("click", (e) => { e.stopPropagation(); reorder(b.id, 1); });
|
||||
|
||||
const del = document.createElement("button");
|
||||
del.className = "pcr-rb-iconbtn"; del.textContent = "✕"; del.title = "Delete region";
|
||||
del.addEventListener("click", (e) => { e.stopPropagation(); removeBox(b.id); });
|
||||
|
||||
row.addEventListener("click", () => { activeId = b.id; draw(); renderList(); });
|
||||
|
||||
row.appendChild(swatch);
|
||||
row.appendChild(name);
|
||||
row.appendChild(up);
|
||||
row.appendChild(down);
|
||||
row.appendChild(del);
|
||||
list.appendChild(row);
|
||||
rowEls.set(b.id, { row, nameEl: name });
|
||||
});
|
||||
}
|
||||
|
||||
// In-place rename keyed by id (not a DOM ref): committing a prior rename may
|
||||
// rebuild the list, so we look the live row up fresh from rowEls afterwards.
|
||||
function startListRename(id) {
|
||||
if (renameId != null) commitCanvasRename(); // close any open canvas rename
|
||||
commitListRename(); // and any other open list rename
|
||||
const i = indexOfId(id);
|
||||
const ref = rowEls.get(id);
|
||||
if (i < 0 || !ref) return;
|
||||
const input = document.createElement("input");
|
||||
input.className = "pcr-rb-nameinput";
|
||||
input.maxLength = 24;
|
||||
input.spellcheck = false;
|
||||
input.value = boxes[i].name;
|
||||
let done = false;
|
||||
const finish = (commit) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
listRenameCommit = null;
|
||||
if (commit) applyRename(id, input.value);
|
||||
else renderList();
|
||||
};
|
||||
listRenameCommit = () => finish(true);
|
||||
input.addEventListener("keydown", (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === "Enter") finish(true);
|
||||
else if (e.key === "Escape") finish(false);
|
||||
});
|
||||
input.addEventListener("blur", () => finish(true));
|
||||
input.addEventListener("click", (e) => e.stopPropagation());
|
||||
ref.row.replaceChild(input, ref.nameEl);
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
// ── mutations (each persists, re-binds where names/order change, redraws) ──
|
||||
function applyRename(id, raw) {
|
||||
const i = indexOfId(id);
|
||||
if (i < 0) return;
|
||||
const next = sanitize(raw);
|
||||
if (!next || next === boxes[i].name) { renderList(); return; }
|
||||
if (boxes.some((b, j) => j !== i && b.name === next)) { renderList(); return; } // keep unique
|
||||
const old = boxes[i].name;
|
||||
boxes[i].name = next;
|
||||
persist();
|
||||
renameRegionBlock(node, old, next); // carry typed tags to the new name
|
||||
bind();
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function recolor(id) {
|
||||
const i = indexOfId(id);
|
||||
if (i < 0) return;
|
||||
const cur = boxes[i].color || PALETTE[i % PALETTE.length];
|
||||
const ci = PALETTE.indexOf(cur);
|
||||
boxes[i].color = PALETTE[(ci + 1) % PALETTE.length];
|
||||
persist();
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function reorder(id, dir) {
|
||||
const i = indexOfId(id);
|
||||
if (i < 0) return;
|
||||
const j = i + dir;
|
||||
if (j < 0 || j >= boxes.length) return;
|
||||
const [b] = boxes.splice(i, 1);
|
||||
boxes.splice(j, 0, b); // array order = mask-row / z order
|
||||
persist();
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function removeBox(id) {
|
||||
const i = indexOfId(id);
|
||||
if (i < 0) return;
|
||||
boxes.splice(i, 1);
|
||||
if (activeId === id) activeId = null;
|
||||
persist();
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function addRegion() {
|
||||
const id = genId();
|
||||
boxes.push({ id, name: uniqueName(), x: 0.3, y: 0.3, w: 0.4, h: 0.4 });
|
||||
activeId = id;
|
||||
persist();
|
||||
bind();
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
addBtn.addEventListener("click", (e) => { e.stopPropagation(); addRegion(); });
|
||||
|
||||
function addRegionAt(x, y) {
|
||||
const id = genId();
|
||||
const w = 0.3, h = 0.3;
|
||||
boxes.push({ id, name: uniqueName(), x: clamp01(x - w / 2), y: clamp01(y - h / 2), w, h });
|
||||
activeId = id;
|
||||
persist(); bind(); draw(); renderList();
|
||||
}
|
||||
|
||||
function openCanvasRename(id) {
|
||||
const i = indexOfId(id);
|
||||
if (i < 0) return;
|
||||
commitListRename();
|
||||
activeId = id; renameId = id;
|
||||
const f = frame();
|
||||
const r = toPx(boxes[i], f);
|
||||
renameInput.value = boxes[i].name;
|
||||
renameInput.style.left = r.x + "px";
|
||||
renameInput.style.top = Math.max(f.fy, r.y - 18) + "px";
|
||||
renameInput.style.width = Math.max(70, Math.min(r.w || 120, 160)) + "px";
|
||||
renameInput.style.display = "block";
|
||||
renameInput.focus(); renameInput.select();
|
||||
draw();
|
||||
}
|
||||
|
||||
// Pull `$name{}` blocks from the bound prompt into boxes (prompt -> node).
|
||||
// Event-driven: on panel open, on rewire, or via the ⟲ button — never polled.
|
||||
function syncFromPrompt() {
|
||||
const { pc } = traceRegionConsumer(node);
|
||||
if (!pc?._pcrEditor) return;
|
||||
const text = pc._pcrEditor.state.doc.toString();
|
||||
const have = new Set(boxes.map((b) => b.name.toLowerCase()));
|
||||
let added = false;
|
||||
for (const m of text.matchAll(/\$(\w+)\s*\{/g)) {
|
||||
const low = m[1].toLowerCase();
|
||||
if (low === "background" || have.has(low)) continue; // background is a scope, not a box
|
||||
boxes.push({ id: genId(), name: sanitize(m[1]), x: 0.3, y: 0.3, w: 0.4, h: 0.4 });
|
||||
have.add(low);
|
||||
added = true;
|
||||
}
|
||||
if (added) { persist(); draw(); renderList(); }
|
||||
}
|
||||
|
||||
// ── right-click context menu (Rename / Recolour / Delete on a box; Add on empty) ──
|
||||
let ctxMenu = null;
|
||||
function closeCtxMenu() { if (ctxMenu) { ctxMenu.remove(); ctxMenu = null; } }
|
||||
function openCtxMenu(clientX, clientY, items) {
|
||||
closeCtxMenu();
|
||||
const menu = document.createElement("div");
|
||||
menu.style.cssText =
|
||||
"position:fixed;z-index:10000;left:" + clientX + "px;top:" + clientY + "px;" +
|
||||
"background:#1b1b22;border:1px solid #3a3a46;border-radius:6px;padding:4px;" +
|
||||
"box-shadow:0 6px 24px rgba(0,0,0,.5);min-width:150px;font:13px sans-serif;";
|
||||
menu.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
for (const it of items) {
|
||||
const row = document.createElement("div");
|
||||
row.textContent = it.label;
|
||||
row.style.cssText = "padding:6px 10px;border-radius:4px;cursor:pointer;color:" +
|
||||
(it.danger ? "#ff8a8a" : "#dfe3ea") + ";";
|
||||
row.addEventListener("mouseenter", () => { row.style.background = "#2c3242"; });
|
||||
row.addEventListener("mouseleave", () => { row.style.background = "transparent"; });
|
||||
row.addEventListener("click", (e) => { e.stopPropagation(); closeCtxMenu(); it.action(); });
|
||||
menu.appendChild(row);
|
||||
}
|
||||
document.body.appendChild(menu);
|
||||
ctxMenu = menu;
|
||||
setTimeout(() => document.addEventListener("pointerdown", closeCtxMenu, { capture: true, once: true }), 0);
|
||||
}
|
||||
function onContextMenu(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // suppress LiteGraph's node menu
|
||||
const { px, py, nx, ny, f } = pointerNorm(e);
|
||||
const i = hitBox(px, py, f);
|
||||
if (i >= 0) {
|
||||
const id = boxes[i].id;
|
||||
activeId = id; draw(); renderList();
|
||||
openCtxMenu(e.clientX, e.clientY, [
|
||||
{ label: "Rename", action: () => openCanvasRename(id) },
|
||||
{ label: "Recolour", action: () => recolor(id) },
|
||||
{ label: "Delete", danger: true, action: () => removeBox(id) },
|
||||
]);
|
||||
} else if (insideFrame(px, py, f)) {
|
||||
openCtxMenu(e.clientX, e.clientY, [
|
||||
{ label: "Add region here", action: () => addRegionAt(nx, ny) },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
let data = [];
|
||||
try {
|
||||
const raw = JSON.parse(widgetValue(node, "box_state") || "[]");
|
||||
data = Array.isArray(raw) ? raw : (raw.boxes || []);
|
||||
} catch { data = []; }
|
||||
boxes = data.filter((b) => b && typeof b.x === "number").map((b) => ({
|
||||
id: b.id || null,
|
||||
name: sanitize(b.name) || "",
|
||||
x: b.x, y: b.y, w: b.w, h: b.h,
|
||||
...(b.color ? { color: b.color } : {}),
|
||||
}));
|
||||
// stable ids: bump the seq past any saved suffix, then fill missing ids
|
||||
for (const b of boxes) {
|
||||
const m = b.id && /^rb(\d+)$/.exec(b.id);
|
||||
if (m) idSeq = Math.max(idSeq, +m[1] + 1);
|
||||
}
|
||||
for (const b of boxes) if (!b.id) b.id = genId();
|
||||
if (activeId != null && indexOfId(activeId) < 0) activeId = null;
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function persist() {
|
||||
setWidgetValue(node, "box_state", JSON.stringify({ boxes }));
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
// Push box names into the bound prompt as $name{} blocks + wire POSE_JSON.
|
||||
// Called on commit (add/rename), not during a drag, so the editor isn't thrashed.
|
||||
function bind() {
|
||||
ensureConsumerPoseWire(node);
|
||||
reconcileRegionBlocks(node, boxes.map((b) => b.name).filter(Boolean));
|
||||
}
|
||||
|
||||
// ── pointer interaction ──
|
||||
let drag = null; // {mode:'new'|'move'|'resize', ...}
|
||||
|
||||
function pointerNorm(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const f = frame();
|
||||
// getBoundingClientRect() is in SCREEN px (scaled by the LiteGraph graph
|
||||
// zoom), but frame()/draw() work in canvas LAYOUT px (clientWidth). Convert
|
||||
// screen->layout so the rubber band, hit-testing and cursor line up at ANY
|
||||
// zoom. Without this, off-100% zoom made drags miss every box (=> "move
|
||||
// creates a new box"), the rubber band drift, and the wrong hover cursor.
|
||||
const sx = (canvas.clientWidth || 1) / (rect.width || 1);
|
||||
const sy = (canvas.clientHeight || 1) / (rect.height || 1);
|
||||
const px = (e.clientX - rect.left) * sx;
|
||||
const py = (e.clientY - rect.top) * sy;
|
||||
return { px, py, f, nx: (px - f.fx) / f.fw, ny: (py - f.fy) / f.fh };
|
||||
}
|
||||
const clamp01 = (v) => Math.min(1, Math.max(0, v));
|
||||
function insideFrame(px, py, f) {
|
||||
return px >= f.fx && px <= f.fx + f.fw && py >= f.fy && py <= f.fy + f.fh;
|
||||
}
|
||||
|
||||
function hitHandle(px, py, f) {
|
||||
const i = activeIndex();
|
||||
if (i < 0) return null;
|
||||
const r = toPx(boxes[i], f);
|
||||
for (const hd of handles(r)) {
|
||||
if (Math.abs(px - hd.x) <= HANDLE_HIT && Math.abs(py - hd.y) <= HANDLE_HIT) return hd.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function hitBox(px, py, f) {
|
||||
for (let i = boxes.length - 1; i >= 0; i--) { // topmost first
|
||||
const r = toPx(boxes[i], f);
|
||||
if (px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.h) return i;
|
||||
const l = labelRect(boxes[i], f); // the $name label is part of the box
|
||||
if (px >= l.x && px <= l.x + l.w && py >= l.y && py <= l.y + l.h) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function updateCursor(px, py, f) {
|
||||
const h = hitHandle(px, py, f);
|
||||
if (h) { canvasWrap.style.cursor = cursorForHandle(h); return; }
|
||||
if (hitBox(px, py, f) >= 0) { canvasWrap.style.cursor = "move"; return; }
|
||||
canvasWrap.style.cursor = insideFrame(px, py, f) ? "crosshair" : "default";
|
||||
}
|
||||
|
||||
function onDown(e) {
|
||||
if (e.button !== 0) return;
|
||||
e.stopPropagation(); // keep litegraph from panning/selecting the node
|
||||
// Release any stale LiteGraph pointer-capture left by a prior node drag —
|
||||
// otherwise it keeps the pointer and our move/resize never sees pointermove.
|
||||
const lg = document.querySelector("canvas.lgraphcanvas");
|
||||
if (lg && typeof e.pointerId === "number") {
|
||||
try { if (lg.hasPointerCapture(e.pointerId)) lg.releasePointerCapture(e.pointerId); } catch {}
|
||||
}
|
||||
container.focus();
|
||||
const { px, py, nx, ny, f } = pointerNorm(e);
|
||||
const h = hitHandle(px, py, f);
|
||||
if (h) {
|
||||
drag = { mode: "resize", handle: h };
|
||||
} else {
|
||||
const i = hitBox(px, py, f);
|
||||
if (i >= 0) {
|
||||
// select first; only turn into a move once the pointer crosses the
|
||||
// dead-zone, so a plain click selects without nudging the box.
|
||||
activeId = boxes[i].id;
|
||||
drag = { mode: "move", ox: nx - boxes[i].x, oy: ny - boxes[i].y, downPx: px, downPy: py, moved: false };
|
||||
renderList();
|
||||
} else if (insideFrame(px, py, f)) {
|
||||
const id = genId();
|
||||
boxes.push({ id, name: "", x: clamp01(nx), y: clamp01(ny), w: 0, h: 0 });
|
||||
activeId = id;
|
||||
drag = { mode: "new", ax: clamp01(nx), ay: clamp01(ny) };
|
||||
} else {
|
||||
activeId = null; // click in the dead margin clears selection
|
||||
renderList();
|
||||
}
|
||||
}
|
||||
canvas.setPointerCapture?.(e.pointerId);
|
||||
draw();
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
const p = pointerNorm(e);
|
||||
if (!drag) { updateCursor(p.px, p.py, p.f); return; }
|
||||
e.stopPropagation();
|
||||
const { px, py, nx, ny } = p;
|
||||
const i = activeIndex();
|
||||
if (i < 0) return;
|
||||
const b = boxes[i];
|
||||
if (drag.mode === "new") {
|
||||
b.x = Math.min(drag.ax, clamp01(nx));
|
||||
b.y = Math.min(drag.ay, clamp01(ny));
|
||||
b.w = Math.abs(clamp01(nx) - drag.ax);
|
||||
b.h = Math.abs(clamp01(ny) - drag.ay);
|
||||
} else if (drag.mode === "move") {
|
||||
if (!drag.moved) {
|
||||
if (Math.abs(px - drag.downPx) < DEADZONE && Math.abs(py - drag.downPy) < DEADZONE) return;
|
||||
drag.moved = true;
|
||||
}
|
||||
b.x = clamp01(nx - drag.ox); if (b.x + b.w > 1) b.x = 1 - b.w;
|
||||
b.y = clamp01(ny - drag.oy); if (b.y + b.h > 1) b.y = 1 - b.h;
|
||||
} else if (drag.mode === "resize") {
|
||||
resize(b, drag.handle, clamp01(nx), clamp01(ny));
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function resize(b, handle, nx, ny) {
|
||||
let x0 = b.x, y0 = b.y, x1 = b.x + b.w, y1 = b.y + b.h;
|
||||
if (handle.includes("w")) x0 = nx;
|
||||
if (handle.includes("e")) x1 = nx;
|
||||
if (handle.includes("n")) y0 = ny;
|
||||
if (handle.includes("s")) y1 = ny;
|
||||
b.x = Math.min(x0, x1); b.w = Math.abs(x1 - x0);
|
||||
b.y = Math.min(y0, y1); b.h = Math.abs(y1 - y0);
|
||||
}
|
||||
|
||||
function onUp(e) {
|
||||
if (!drag) return;
|
||||
e.stopPropagation();
|
||||
canvas.releasePointerCapture?.(e.pointerId);
|
||||
const wasNew = drag.mode === "new";
|
||||
const moved = drag.moved === true;
|
||||
const resized = drag.mode === "resize";
|
||||
drag = null;
|
||||
const i = activeIndex();
|
||||
const b = i >= 0 ? boxes[i] : null;
|
||||
if (wasNew && b && (b.w < MIN_BOX || b.h < MIN_BOX)) {
|
||||
boxes.splice(i, 1); activeId = null; // discard a micro-drag (a click on empty frame)
|
||||
persist();
|
||||
} else if (wasNew && b) {
|
||||
if (!b.name) b.name = uniqueName();
|
||||
persist();
|
||||
bind(); // new region: seed its $name{} block + wire POSE_JSON
|
||||
} else if (moved || resized) {
|
||||
persist(); // move/resize: geometry changed, names unchanged
|
||||
}
|
||||
// a pure select click (move mode, dead-zone never crossed) changes nothing
|
||||
draw();
|
||||
renderList();
|
||||
}
|
||||
|
||||
function onDblClick(e) {
|
||||
e.stopPropagation();
|
||||
if (renameId != null) commitCanvasRename(); // commit any open canvas rename
|
||||
const { px, py, f } = pointerNorm(e);
|
||||
const i = hitBox(px, py, f);
|
||||
if (i < 0) return;
|
||||
openCanvasRename(boxes[i].id);
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if ((e.key === "Delete" || e.key === "Backspace") && activeId != null && renameId == null) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeBox(activeId);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.addEventListener("pointerdown", onDown);
|
||||
canvas.addEventListener("pointermove", onMove);
|
||||
canvas.addEventListener("pointerup", onUp);
|
||||
canvas.addEventListener("pointercancel", onUp);
|
||||
canvas.addEventListener("dblclick", onDblClick);
|
||||
canvas.addEventListener("contextmenu", onContextMenu);
|
||||
canvas.addEventListener("wheel", (e) => e.stopPropagation(), { passive: true });
|
||||
container.addEventListener("keydown", onKey);
|
||||
|
||||
// Responsive: show the region list only when there's room; always redraw the
|
||||
// canvas to its new size. Event-driven via ResizeObserver (no polling).
|
||||
function applyResponsive() {
|
||||
sidebar.style.display = container.clientWidth >= SIDEBAR_SHOW_WIDTH ? "flex" : "none";
|
||||
if (renameId != null) commitCanvasRename(); // commit (not discard) — reflow would float it
|
||||
draw();
|
||||
}
|
||||
const ro = new ResizeObserver(() => applyResponsive());
|
||||
ro.observe(container);
|
||||
|
||||
// ── dock into the Region Box panel (relocate-and-restore, mirrors the Poser) ──
|
||||
// The !important flow styles beat ComfyUI's per-frame DOM-widget writes (it
|
||||
// still holds this element's ref and would otherwise re-absolute-position/hide).
|
||||
let dockActive = false, dockSavedCss = "", dockParent = null, dockNextSibling = null;
|
||||
const enterDock = (panelBodyEl) => {
|
||||
if (!panelBodyEl) return;
|
||||
if (dockActive) exitDock(); // re-dock into a different panel cleanly
|
||||
dockActive = true;
|
||||
dockSavedCss = container.style.cssText;
|
||||
dockParent = container.parentNode;
|
||||
dockNextSibling = container.nextSibling;
|
||||
panelBodyEl.appendChild(container);
|
||||
container.style.cssText =
|
||||
"position:relative !important;left:0 !important;top:0 !important;" +
|
||||
"width:100% !important;height:100% !important;transform:none !important;" +
|
||||
"margin:0 !important;max-width:none !important;max-height:none !important;" +
|
||||
"display:flex !important;flex-direction:row !important;visibility:visible !important;opacity:1 !important;" +
|
||||
"background:#0d0d11;border-radius:0;overflow:hidden;touch-action:none;";
|
||||
applyResponsive();
|
||||
requestAnimationFrame(() => syncFromPrompt()); // opening the panel pulls in $name{} blocks
|
||||
};
|
||||
const exitDock = () => {
|
||||
if (!dockActive) return;
|
||||
dockActive = false;
|
||||
if (dockParent && dockParent.isConnected) {
|
||||
if (dockNextSibling && dockNextSibling.parentNode === dockParent) dockParent.insertBefore(container, dockNextSibling);
|
||||
else dockParent.appendChild(container);
|
||||
} else {
|
||||
container.remove(); // home slot gone (node removed while docked)
|
||||
}
|
||||
dockParent = dockNextSibling = null;
|
||||
container.style.cssText = dockSavedCss;
|
||||
applyResponsive();
|
||||
};
|
||||
node._pcrRegion = { enterDock, exitDock, container };
|
||||
|
||||
// Adopt the downstream latent's resolution; re-sync when this node is rewired.
|
||||
const origOnConnectionsChange = node.onConnectionsChange;
|
||||
node.onConnectionsChange = function (...args) {
|
||||
origOnConnectionsChange?.apply(this, args);
|
||||
requestAnimationFrame(() => syncToLatent(node));
|
||||
};
|
||||
// Reload boxes after the workflow restores the hidden widget value.
|
||||
const origOnConfigure = node.onConfigure;
|
||||
node.onConfigure = function (...args) {
|
||||
origOnConfigure?.apply(this, args);
|
||||
load();
|
||||
};
|
||||
|
||||
node.computeSize = (out) => {
|
||||
if (out) {
|
||||
const cur = node.size || MIN_NODE_SIZE;
|
||||
return [Math.max(MIN_NODE_SIZE[0], cur[0]), Math.max(MIN_NODE_SIZE[1], cur[1])];
|
||||
}
|
||||
return [...MIN_NODE_SIZE];
|
||||
};
|
||||
if (!node.size || node.size[0] < MIN_NODE_SIZE[0] || node.size[1] < MIN_NODE_SIZE[1]) {
|
||||
node.setSize?.(node.computeSize());
|
||||
}
|
||||
|
||||
const origOnRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
origOnRemoved?.call(this);
|
||||
node._pcrAlive = false;
|
||||
regionBoxNodes.delete(node);
|
||||
regionBoxRegistry.remove(node); // un-gate panels; open panels re-dock or show empty
|
||||
closeCtxMenu();
|
||||
try { node._pcrRegion?.exitDock?.(); } catch {}
|
||||
node._pcrRegion = null;
|
||||
ro.disconnect();
|
||||
};
|
||||
|
||||
load();
|
||||
requestAnimationFrame(() => { load(); applyResponsive(); syncToLatent(node); regionBoxRegistry.signalReady(node); });
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "PromptChain.RegionBox",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== NODE_TYPE) return;
|
||||
setupRegionBox(node);
|
||||
},
|
||||
});
|
||||
+248
-30
@@ -2,36 +2,222 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from comfy_api.latest import io
|
||||
|
||||
logger = logging.getLogger("promptchain.ideogram_caption")
|
||||
|
||||
|
||||
def _envelope(text: str) -> str:
|
||||
"""Wrap plain text in Ideogram's minimal JSON caption envelope.
|
||||
def _caption(text: str) -> str:
|
||||
"""Wrap a plain PromptChain prompt in a COMPLETE Ideogram JSON caption.
|
||||
|
||||
Ideogram 4 was trained on structured JSON captions; its docs state the
|
||||
'Image blocked by safety filter' false-positive rate is HIGHER for non-JSON
|
||||
prompts. Measured on a benign prompt: plain text blocked 4/5 seeds, this
|
||||
mechanical envelope (coherent text) blocked 1/5. The remaining ~20% is
|
||||
cleared by the auto-retry sampler. (A real per-element deconstruction via an
|
||||
LLM gets to 0/5 — that's the optional upgrade when a local LLM is set up.)
|
||||
Ideogram 4 was trained on structured JSON captions and its built-in safety
|
||||
*false-positive* rate is far higher for non-conforming prompts (official:
|
||||
"false positive rates ... higher for non-json like prompts"). Empirically,
|
||||
on flagged content (e.g. a swimsuit/rear-pose character): plain text AND a
|
||||
JSON wrapper with an EMPTY `elements` array both block ~100% across seeds,
|
||||
while a COMPLETE caption — high_level_description + background + at least one
|
||||
real `element` — passes ~100% regardless of where the content sits (HLD,
|
||||
background, or element). So the whole prompt goes into one element's `desc`
|
||||
under a benign summary: that completeness is what makes the model read it as
|
||||
a conforming caption rather than falling back to the non-JSON safety path.
|
||||
"""
|
||||
t = (text or "").strip()
|
||||
return json.dumps(
|
||||
{"high_level_description": t,
|
||||
"compositional_deconstruction": {"background": t, "elements": []}},
|
||||
ensure_ascii=False)
|
||||
# The element bbox must be an INSET subregion: empirically a full-frame
|
||||
# [0,0,1000,1000] bbox AND omitting bbox both block ~100% on flagged content,
|
||||
# while an inset region passes (the model reads it as "an object placed in a
|
||||
# scene" rather than "the whole image IS this object"). Centred, with margins
|
||||
# on all sides; the desc still drives the full composition.
|
||||
return json.dumps({
|
||||
"high_level_description": "A high-quality, detailed image.",
|
||||
"compositional_deconstruction": {
|
||||
"background": "a fitting background that suits the subject",
|
||||
"elements": [{"type": "obj", "bbox": [60, 250, 990, 770], "desc": t}],
|
||||
},
|
||||
}, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
# Ideogram bbox is [y_min, x_min, y_max, x_max] on a 0-1000 grid, top-left origin.
|
||||
_FULL_FRAME_INSET = [60, 250, 990, 770] # the proven single-element box
|
||||
|
||||
|
||||
def _inset_bbox(bbox) -> list[int]:
|
||||
"""Clamp a box to 0-1000 and guarantee it is an INSET (never full-frame).
|
||||
|
||||
A full-frame element re-trips the safety false-positive (the model reads it
|
||||
as "the whole image IS this object"). A user-drawn box is normally inset
|
||||
already; this only rescues an edge-to-edge box."""
|
||||
try:
|
||||
y0, x0, y1, x1 = (int(round(v)) for v in bbox)
|
||||
except (TypeError, ValueError):
|
||||
return list(_FULL_FRAME_INSET)
|
||||
y0, y1 = sorted((max(0, min(1000, y0)), max(0, min(1000, y1))))
|
||||
x0, x1 = sorted((max(0, min(1000, x0)), max(0, min(1000, x1))))
|
||||
# Degenerate or effectively full-frame -> fall back to the proven inset.
|
||||
if x1 - x0 < 10 or y1 - y0 < 10 or (x1 - x0 >= 990 and y1 - y0 >= 990):
|
||||
return list(_FULL_FRAME_INSET)
|
||||
return [y0, x0, y1, x1]
|
||||
|
||||
|
||||
def _load(obj):
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
if not (obj and isinstance(obj, str) and obj.strip()):
|
||||
return {}
|
||||
try:
|
||||
return json.loads(obj)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {}
|
||||
|
||||
|
||||
# A leading quoted literal ('...' or "...") in a `$name{}` region's BODY marks a
|
||||
# TEXT element — Ideogram renders that exact string (its headline feature). This
|
||||
# is a property of a real scope (the region body), never of a comment.
|
||||
_LITERAL_RE = re.compile(r"""^\s*['"](.+?)['"]\s*(.*)$""", re.DOTALL)
|
||||
|
||||
|
||||
def _split_literal(s: str):
|
||||
"""('LITERAL', rest-as-desc) if s starts with a quoted literal, else (None, s)."""
|
||||
m = _LITERAL_RE.match(s or "")
|
||||
if m:
|
||||
return m.group(1).strip(), m.group(2).strip()
|
||||
return None, (s or "").strip()
|
||||
|
||||
|
||||
def _mk_obj(e: dict) -> dict:
|
||||
"""obj element in spec key order: type, bbox?, desc."""
|
||||
o: dict = {"type": "obj"}
|
||||
if e.get("bbox") is not None:
|
||||
o["bbox"] = e["bbox"]
|
||||
o["desc"] = e.get("desc", "")
|
||||
return o
|
||||
|
||||
|
||||
def _mk_text(e: dict) -> dict:
|
||||
"""text element in spec key order: type, bbox?, text, desc."""
|
||||
o: dict = {"type": "text"}
|
||||
if e.get("bbox") is not None:
|
||||
o["bbox"] = e["bbox"]
|
||||
o["text"] = e.get("text", "")
|
||||
o["desc"] = e.get("desc") or f'the text "{e.get("text", "")}"'
|
||||
return o
|
||||
|
||||
|
||||
def _pos_word(bbox) -> str:
|
||||
"""A rough placement word from a 0-1000 [y0,x0,y1,x1] box, for the HLD."""
|
||||
if not (isinstance(bbox, (list, tuple)) and len(bbox) == 4):
|
||||
return ""
|
||||
xc = (bbox[1] + bbox[3]) / 2.0
|
||||
yc = (bbox[0] + bbox[2]) / 2.0
|
||||
h = "left" if xc < 400 else "right" if xc > 600 else ""
|
||||
v = "top" if yc < 350 else "bottom" if yc > 650 else ""
|
||||
if h and v:
|
||||
return f"{v} {h}"
|
||||
return h or v or "centre"
|
||||
|
||||
|
||||
def _build_caption(prompt_text: str, region_list: list, box_by_name: dict,
|
||||
background_text: str = "") -> str | None:
|
||||
"""Build a conforming Ideogram caption from the prompt text + `$name{}` regions.
|
||||
|
||||
Comments are NOT parsed — `prompt_text` is the already comment-stripped prompt
|
||||
(minus the regions). Structure comes ONLY from real scopes: the reserved
|
||||
`$background{ ... }` region -> the `background` field; each other `$name{}`
|
||||
region -> one element placed at its drawn box (bbox optional, matched by name);
|
||||
a region whose body starts with a quoted literal `"..."` -> a `type:"text"`
|
||||
element (Ideogram renders that exact string). The remaining prompt text ->
|
||||
high_level_description. A complete caption (HLD + background + >=1 INSET-bbox
|
||||
element) keeps the safety bypass, so we anchor one element (preferring a
|
||||
subject) with the proven inset when no drawn box supplied one. Returns None
|
||||
when there's nothing to build.
|
||||
"""
|
||||
raw_obj: list[dict] = []
|
||||
raw_text: list[dict] = []
|
||||
for r in region_list:
|
||||
name = str(r.get("name", "")).lower()
|
||||
box = box_by_name.get(name)
|
||||
d = (r.get("text") or "").strip()
|
||||
if not d:
|
||||
if box is None:
|
||||
continue # nothing to place; the HLD covers it
|
||||
d = name or "subject" # box drawn but no text: keep the placement
|
||||
bbox = _inset_bbox(box) if box is not None else None
|
||||
lit, rest = _split_literal(d)
|
||||
e = {"text": lit, "desc": rest} if lit is not None else {"desc": d}
|
||||
if bbox is not None:
|
||||
e["bbox"] = bbox
|
||||
(raw_text if lit is not None else raw_obj).append(e)
|
||||
|
||||
prompt_text = (prompt_text or "").strip()
|
||||
if raw_obj or raw_text:
|
||||
# Ideogram leans HARD on the high_level_description; a STYLE-ONLY HLD lets
|
||||
# it free-compose (e.g. a whole wedding party instead of one woman + one
|
||||
# man). Weave the subjects + their box positions into the summary so the
|
||||
# count and placement actually stick.
|
||||
phrases = []
|
||||
for e in raw_obj + raw_text:
|
||||
d = (e.get("desc") or e.get("text") or "").strip()
|
||||
if not d:
|
||||
continue
|
||||
pos = _pos_word(e.get("bbox"))
|
||||
phrases.append(f"{d} on the {pos}" if pos else d)
|
||||
summary = ", and ".join(phrases)
|
||||
if summary and prompt_text:
|
||||
hld = f"{prompt_text.rstrip(' .')}, showing {summary}."
|
||||
elif summary:
|
||||
hld = summary[:1].upper() + summary[1:] + "."
|
||||
else:
|
||||
hld = prompt_text or "A high-quality, detailed image."
|
||||
else:
|
||||
if not prompt_text:
|
||||
return None
|
||||
raw_obj = [{"desc": prompt_text}] # no regions: the prompt is the subject
|
||||
hld = "A high-quality, detailed image."
|
||||
|
||||
# safety: guarantee >=1 element carries an inset bbox (a boxless / full-frame
|
||||
# caption re-trips the safety false-positive). Prefer anchoring a subject.
|
||||
if not any("bbox" in e for e in raw_obj + raw_text):
|
||||
(raw_obj[0] if raw_obj else raw_text[0])["bbox"] = list(_FULL_FRAME_INSET)
|
||||
|
||||
elements = [_mk_obj(o) for o in raw_obj] + [_mk_text(t) for t in raw_text]
|
||||
background = (background_text or "").strip() or "a fitting background that suits the subject"
|
||||
cap = {
|
||||
"high_level_description": hld,
|
||||
"compositional_deconstruction": {
|
||||
"background": background,
|
||||
"elements": elements,
|
||||
},
|
||||
}
|
||||
return json.dumps(cap, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def _needs_wrapping(s: str) -> bool:
|
||||
"""A hand-written caption is left alone ONLY if it's valid JSON with a
|
||||
non-empty elements array — an empty-elements caption is the exact thing that
|
||||
trips the safety false-positive, so we re-wrap it."""
|
||||
if not (s.startswith("{") and s.endswith("}")):
|
||||
return True
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
return True
|
||||
els = (obj.get("compositional_deconstruction") or {}).get("elements")
|
||||
return not (isinstance(els, list) and len(els) > 0)
|
||||
|
||||
|
||||
class IdeogramCaptionNode(io.ComfyNode):
|
||||
"""Turn a PromptChain prompt into an Ideogram-friendly JSON caption.
|
||||
"""Turn a PromptChain prompt into a conforming Ideogram JSON caption.
|
||||
|
||||
Sits between PromptChain's `positive` output and CLIPTextEncode in the
|
||||
Ideogram template. JSON-shaped prompts trip Ideogram's stochastic safety
|
||||
refusal far less often than plain text. If the user already wrote valid JSON,
|
||||
it's passed through untouched.
|
||||
Ideogram template. A complete structured caption trips Ideogram's stochastic
|
||||
safety refusal far less than plain text (the model treats non-conforming
|
||||
prompts as the high-false-positive path). A hand-written caption that already
|
||||
has elements is passed through untouched.
|
||||
|
||||
Optional regional inputs: wire PromptChain's `regions` output and a
|
||||
PromptChain_RegionBox `POSE_JSON` to turn each drawn, named box into a caption
|
||||
element with its own bbox — visual region guidance for Ideogram.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -43,9 +229,18 @@ class IdeogramCaptionNode(io.ComfyNode):
|
||||
inputs=[
|
||||
io.String.Input("text", default="", force_input=True,
|
||||
tooltip="Wire to Prompt Chain's 'positive' output."),
|
||||
io.Combo.Input("mode", options=["envelope", "off"], default="envelope",
|
||||
tooltip="envelope = wrap plain text in Ideogram's JSON caption "
|
||||
"(reduces the model's false safety blocks); off = passthrough."),
|
||||
io.Combo.Input("mode", options=["caption", "off"], default="caption",
|
||||
tooltip="caption = wrap the prompt in a complete Ideogram JSON "
|
||||
"caption (avoids the model's false safety blocks); "
|
||||
"off = passthrough."),
|
||||
# Optional regional pair. When both are wired, each $name{} region
|
||||
# becomes a caption element placed at its drawn box.
|
||||
io.String.Input("regions", default="", optional=True, force_input=True,
|
||||
tooltip="Optional: wire Prompt Chain's 'regions' output (4th) "
|
||||
"to place each $name{} block at its Region Box."),
|
||||
io.String.Input("pose", default="", optional=True, force_input=True,
|
||||
tooltip="Optional: wire a Region Box POSE_JSON — carries each "
|
||||
"region's box (bbox) so the caption places it."),
|
||||
],
|
||||
outputs=[
|
||||
io.String.Output("text"),
|
||||
@@ -53,15 +248,38 @@ class IdeogramCaptionNode(io.ComfyNode):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, text: str = "", mode: str = "envelope") -> io.NodeOutput:
|
||||
s = (text or "").strip()
|
||||
if mode == "off" or not s:
|
||||
def execute(cls, text: str = "", mode: str = "caption",
|
||||
regions: str = "", pose: str = "") -> io.NodeOutput:
|
||||
if mode == "off":
|
||||
return io.NodeOutput(text)
|
||||
# Already valid JSON (user hand-wrote a caption) — leave it alone.
|
||||
if s.startswith("{") and s.endswith("}"):
|
||||
try:
|
||||
json.loads(s)
|
||||
return io.NodeOutput(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return io.NodeOutput(_envelope(text))
|
||||
s = (text or "").strip()
|
||||
if s and not _needs_wrapping(s):
|
||||
return io.NodeOutput(text) # already a complete caption
|
||||
rdata = _load(regions)
|
||||
region_list = rdata.get("regions") or []
|
||||
# `global` is the prompt with comments stripped and the $name{} regions
|
||||
# removed; fall back to the plain positive text when regions aren't wired.
|
||||
prompt_text = rdata.get("global")
|
||||
if prompt_text is None:
|
||||
prompt_text = text
|
||||
# Reserved `$background{ ... }` scope -> the Ideogram background field;
|
||||
# every other $name{} region is a subject element.
|
||||
background_text = ""
|
||||
subjects = []
|
||||
for r in region_list:
|
||||
if str(r.get("name", "")).lower() == "background":
|
||||
background_text = (r.get("text") or "").strip() or background_text
|
||||
else:
|
||||
subjects.append(r)
|
||||
box_by_name = {}
|
||||
for e in (_load(pose).get("regionEntities") or []):
|
||||
if isinstance(e, dict) and e.get("name") and isinstance(e.get("bbox"), (list, tuple)):
|
||||
box_by_name[str(e["name"]).lower()] = e["bbox"]
|
||||
# Caption from the prompt text + $name{} region scopes only. Comments are
|
||||
# never parsed for structure — they are stripped by the compiler.
|
||||
caption = _build_caption(prompt_text, subjects, box_by_name, background_text)
|
||||
if caption:
|
||||
return io.NodeOutput(caption)
|
||||
if not s:
|
||||
return io.NodeOutput(text)
|
||||
return io.NodeOutput(_caption(text))
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from comfy_api.latest import io
|
||||
|
||||
|
||||
def _parse_boxes(box_state: str) -> list[dict]:
|
||||
"""Read the canvas's serialized boxes: [{name, x, y, w, h}] normalized 0-1,
|
||||
top-left origin. Tolerant of a bare list or a {boxes:[...]} wrapper, and
|
||||
skips malformed entries rather than 500-ing the node."""
|
||||
try:
|
||||
data = json.loads(box_state) if (box_state and box_state.strip()) else []
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
raw = data.get("boxes", []) if isinstance(data, dict) else data
|
||||
out = []
|
||||
if isinstance(raw, list):
|
||||
for b in raw:
|
||||
if not isinstance(b, dict):
|
||||
continue
|
||||
try:
|
||||
x, y, w, h = float(b["x"]), float(b["y"]), float(b["w"]), float(b["h"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
out.append({"name": str(b.get("name", "")).strip(), "x": x, "y": y, "w": w, "h": h})
|
||||
return out
|
||||
|
||||
|
||||
def _feather(mask: torch.Tensor, px: int) -> torch.Tensor:
|
||||
"""Soft falloff on a [1,H,W] mask via a separable gaussian.
|
||||
|
||||
Hard rectangle masks seam on Flux regional conditioning (its mask blend has
|
||||
no implicit smoothing); a few px of feather removes the visible region edge.
|
||||
px<=0 keeps the mask hard — SDXL attention-couple tolerates hard edges, and
|
||||
the consumers' own mask_dilation already grows the region outward.
|
||||
"""
|
||||
if px <= 0:
|
||||
return mask
|
||||
radius = int(px)
|
||||
sigma = max(px / 2.0, 0.5)
|
||||
xs = torch.arange(-radius, radius + 1, dtype=torch.float32)
|
||||
k = torch.exp(-(xs ** 2) / (2.0 * sigma * sigma))
|
||||
k = k / k.sum()
|
||||
m = mask.unsqueeze(0) # [1,1,H,W]
|
||||
m = F.conv2d(m, k.view(1, 1, -1, 1), padding=(radius, 0))
|
||||
m = F.conv2d(m, k.view(1, 1, 1, -1), padding=(0, radius))
|
||||
return m.squeeze(0).clamp(0.0, 1.0)
|
||||
|
||||
|
||||
class RegionBoxNode(io.ComfyNode):
|
||||
"""Drawn region rectangles -> per-region masks (+ entity order).
|
||||
|
||||
A canvas in the node body lets you draw and name rectangles; each becomes a
|
||||
rectangular mask at the generation resolution. The MASKS + POSE_JSON outputs
|
||||
drop into PromptChain_AttentionCouple (SDXL) or PromptChain_RegionalConditioning
|
||||
(Flux) exactly like the 3D Poser's outputs, so each `$name{}` block in the
|
||||
prompt paints only inside its box. The heavy work is just rasterizing
|
||||
rectangles, so it happens here from the serialized box list — the canvas does
|
||||
no rendering.
|
||||
|
||||
Mask rows AND regionEntities are built in ONE pass so their order is
|
||||
identical (the consumers bind a `$name{}` block to a mask row by entity
|
||||
order/name). fingerprint_inputs hashes the box layout so editing a box
|
||||
invalidates the sampler cache.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="PromptChain_RegionBox",
|
||||
display_name="PromptChain Region Boxes",
|
||||
category="promptchain",
|
||||
inputs=[
|
||||
# Serialized boxes (JSON), written by the canvas. The frontend
|
||||
# hides this widget (the canvas is the editor, not a text field)
|
||||
# but keeps it serializable so the layout round-trips into the
|
||||
# workflow and feeds the fingerprint.
|
||||
io.String.Input("box_state", default="", socketless=True),
|
||||
# Mask resolution. The canvas letterboxes to this aspect so what
|
||||
# you draw is the output frame; auto-synced to the generation
|
||||
# latent (e.g. SDXL 832×1216 portrait).
|
||||
io.Int.Input("width", default=832, min=64, max=4096, step=8),
|
||||
io.Int.Input("height", default=1216, min=64, max=4096, step=8),
|
||||
io.Int.Input("feather", default=0, min=0, max=128, step=1,
|
||||
tooltip="Soft edge in px. 0 = hard rectangles (fine for SDXL "
|
||||
"attention couple); raise to ~24-48 to remove region "
|
||||
"seams on Flux regional conditioning."),
|
||||
],
|
||||
outputs=[
|
||||
# Per-region rectangular masks in box order. Empty layout -> one
|
||||
# full-canvas mask so a downstream regional node stays valid.
|
||||
io.Mask.Output("MASKS"),
|
||||
# Region entities in mask-row order (the binding contract the
|
||||
# consumers read via their `pose` input).
|
||||
io.String.Output("POSE_JSON"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, box_state: str = "", width: int = 832, height: int = 1216,
|
||||
feather: int = 0) -> io.NodeOutput:
|
||||
boxes = _parse_boxes(box_state)
|
||||
rows: list[torch.Tensor] = []
|
||||
entities: list[dict] = []
|
||||
for i, b in enumerate(boxes):
|
||||
m = torch.zeros((1, height, width), dtype=torch.float32)
|
||||
x0 = int(round(min(max(b["x"], 0.0), 1.0) * width))
|
||||
y0 = int(round(min(max(b["y"], 0.0), 1.0) * height))
|
||||
x1 = int(round(min(max(b["x"] + b["w"], 0.0), 1.0) * width))
|
||||
y1 = int(round(min(max(b["y"] + b["h"], 0.0), 1.0) * height))
|
||||
x0, x1 = sorted((x0, x1))
|
||||
y0, y1 = sorted((y0, y1))
|
||||
if x1 > x0 and y1 > y0:
|
||||
m[0, y0:y1, x0:x1] = 1.0
|
||||
rows.append(_feather(m, feather))
|
||||
# kind=figure: the binding/orphan logic counts figures and clamps
|
||||
# unmatched blocks into figure space — a drawn region is a figure-role
|
||||
# entity (a named prop is a poser-only concept).
|
||||
# bbox: the same rect in Ideogram's caption form [y_min,x_min,y_max,x_max]
|
||||
# on a 0-1000 grid (top-left origin). Mask consumers ignore it; the
|
||||
# Ideogram caption node uses it to place each region's element.
|
||||
iy0 = int(round(min(max(b["y"], 0.0), 1.0) * 1000))
|
||||
ix0 = int(round(min(max(b["x"], 0.0), 1.0) * 1000))
|
||||
iy1 = int(round(min(max(b["y"] + b["h"], 0.0), 1.0) * 1000))
|
||||
ix1 = int(round(min(max(b["x"] + b["w"], 0.0), 1.0) * 1000))
|
||||
iy0, iy1 = sorted((iy0, iy1))
|
||||
ix0, ix1 = sorted((ix0, ix1))
|
||||
entities.append({
|
||||
"name": (b["name"] or f"region{i + 1}").lower(),
|
||||
"kind": "figure",
|
||||
"bbox": [iy0, ix0, iy1, ix1],
|
||||
})
|
||||
|
||||
if not rows:
|
||||
masks = torch.ones((1, height, width), dtype=torch.float32)
|
||||
else:
|
||||
masks = torch.cat(rows, dim=0)
|
||||
|
||||
pose_json = json.dumps({"version": 3, "regionEntities": entities})
|
||||
return io.NodeOutput(masks, pose_json)
|
||||
|
||||
@classmethod
|
||||
def fingerprint_inputs(cls, box_state: str = "", width: int = 832,
|
||||
height: int = 1216, feather: int = 0):
|
||||
m = hashlib.sha256()
|
||||
m.update((box_state or "").encode("utf-8"))
|
||||
m.update(f"{width}x{height}x{feather}".encode("utf-8"))
|
||||
return m.digest().hex()
|
||||
@@ -529,8 +529,10 @@ class RegionalDetailerNode(io.ComfyNode):
|
||||
log.info("image %d: %d face(s) detected", b, len(detected))
|
||||
faces_bound = []
|
||||
for x1, y1, x2, y2 in detected:
|
||||
x1 = max(0, x1 - bbox_dilation); y1 = max(0, y1 - bbox_dilation)
|
||||
x2 = min(img_w, x2 + bbox_dilation); y2 = min(img_h, y2 + bbox_dilation)
|
||||
x1 = max(0, x1 - bbox_dilation)
|
||||
y1 = max(0, y1 - bbox_dilation)
|
||||
x2 = min(img_w, x2 + bbox_dilation)
|
||||
y2 = min(img_h, y2 + bbox_dilation)
|
||||
ridx = _match_region((x1, y1, x2, y2), region_masks) if region_masks is not None else -1
|
||||
faces_bound.append(((x1, y1, x2, y2), ridx))
|
||||
for (x1, y1, x2, y2), ridx in faces_bound:
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# PromptChain — ComfyUI custom node pack.
|
||||
# ComfyUI Registry manifest: https://docs.comfy.org/registry/publishing
|
||||
# `dependencies` is kept in sync with requirements.txt (run `comfy node init`,
|
||||
# or it is mirrored manually) — requirements.txt is the source of truth.
|
||||
|
||||
[project]
|
||||
name = "comfyui-promptchain"
|
||||
version = "0.1.1"
|
||||
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"
|
||||
# Mirrors requirements.txt (the source of truth): core load-time dep + optional
|
||||
# feature deps (PuLID / Face Detailer / AI Selection), declared so they install
|
||||
# at install time instead of the node running pip at runtime.
|
||||
dependencies = [
|
||||
"typing_extensions",
|
||||
"insightface>=1.0",
|
||||
"onnxruntime",
|
||||
"facexlib",
|
||||
"timm",
|
||||
"ftfy",
|
||||
"ultralytics",
|
||||
"scikit-image",
|
||||
"pi-heif",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/mobcat40/ComfyUI-PromptChain"
|
||||
|
||||
[tool.comfy]
|
||||
# Publisher created at registry.comfy.org (immutable; appears in the node URL).
|
||||
PublisherId = "mobcat40"
|
||||
DisplayName = "ComfyUI-PromptChain"
|
||||
Icon = ""
|
||||
@@ -0,0 +1,27 @@
|
||||
# PromptChain — dependencies for the ComfyUI Registry.
|
||||
#
|
||||
# Two groups:
|
||||
# 1. Core load-time deps PromptChain's own code imports (ComfyUI ships the
|
||||
# rest: torch, numpy, Pillow, transformers, einops, av, requests, ...).
|
||||
# 2. Optional feature deps, declared so they install WITH the pack (Manager /
|
||||
# pip at install time) instead of the node running pip at runtime — which
|
||||
# the ComfyUI Registry forbids. These cover PuLID, the Face Detailer, and
|
||||
# AI Selection. They make a fresh install heavier but keep it compliant.
|
||||
#
|
||||
# Torch/numpy/opencv-downgrading pins from the bundled packs (numpy<2,
|
||||
# ml_dtypes, onnxruntime-gpu, facenet-pytorch) are deliberately NOT mirrored
|
||||
# here, so `pip install -r requirements.txt` can never break a working ComfyUI.
|
||||
# The few features that need those (SeedVR2, Flux PuLID) keep their own path.
|
||||
|
||||
# --- core ---
|
||||
typing_extensions
|
||||
|
||||
# --- optional feature deps (PuLID · Face Detailer · AI Selection) ---
|
||||
insightface>=1.0
|
||||
onnxruntime
|
||||
facexlib
|
||||
timm
|
||||
ftfy
|
||||
ultralytics
|
||||
scikit-image
|
||||
pi-heif
|
||||
@@ -19,7 +19,7 @@ The output schema is intentionally tiny:
|
||||
...
|
||||
|
||||
Run:
|
||||
cd C:/comfyui/comfyui/custom_nodes/ComfyUI-PromptChain
|
||||
cd <ComfyUI>/custom_nodes/ComfyUI-PromptChain
|
||||
python scripts/natlang_decompose_probe.py
|
||||
python scripts/natlang_decompose_probe.py <filter>
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ any character/outfit/scene tokens baked in. Same prompt works for any
|
||||
named characters, any outfits, any scene.
|
||||
|
||||
Run:
|
||||
cd C:/comfyui/comfyui/custom_nodes/ComfyUI-PromptChain
|
||||
cd <ComfyUI>/custom_nodes/ComfyUI-PromptChain
|
||||
python scripts/natlang_scene_composer.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
<script>
|
||||
// "Extend Video" modal — renders the next clip continuing from the source
|
||||
// video's last frame (an I2V continuation). The graph is built + queued in the
|
||||
// background (extend-background.js); the result records as a lineage child.
|
||||
// Sophisticated controls (recipe, embedded prompt code editor, sampler knobs)
|
||||
// mirror the upscale/inpaint editor modals; everything is default-preserving —
|
||||
// the seeds come from the source clip + the chosen recipe's template.
|
||||
|
||||
import SettingsSlider from "./model/SettingsSlider.svelte";
|
||||
|
||||
let {
|
||||
open = false,
|
||||
lastFrameUrl = "",
|
||||
fps = 16,
|
||||
frameCount = 0,
|
||||
lightningAvailable = true, // false = the LightX2V speed LoRAs aren't on disk
|
||||
defaults = null, // {width,height,length,fps} seeded from the source clip
|
||||
recipeDefaults = null, // {normal:{steps,cfg,shift,sampler,scheduler}, lightning:{…}}
|
||||
mountPromptEditor = null, // (container, initialValue, onChange, getModelInfo) => EditorView
|
||||
modelInfo = null, // {hash, architecture} for the editor's @prompt autocomplete
|
||||
progress = null,
|
||||
onRun,
|
||||
onDone, // () => land on the result + close (the "jump")
|
||||
onCancel,
|
||||
} = $props();
|
||||
|
||||
// Wan's official negative — folded into the editor as an editable section so an
|
||||
// empty negative at CFG>1 can't let the clip drift to a static "live wallpaper".
|
||||
const WAN_NEG = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走";
|
||||
const SAMPLERS = ["euler", "euler_ancestral", "dpmpp_2m", "dpmpp_2m_sde", "dpmpp_3m_sde", "uni_pc", "lcm"];
|
||||
const SCHEDULERS = ["simple", "normal", "beta", "karras", "sgm_uniform", "ddim_uniform"];
|
||||
|
||||
let recipe = $state("normal"); // "normal" = vanilla 20-step | "lightning" = 4-step distill
|
||||
let stitch = $state(true); // join source + continuation into one continuous file
|
||||
let advancedOpen = $state(false);
|
||||
|
||||
// Adjustable params — seeded from `defaults` (clip) + `recipeDefaults` (sampler).
|
||||
let width = $state(640);
|
||||
let height = $state(640);
|
||||
let length = $state(81);
|
||||
let outFps = $state(16);
|
||||
let steps = $state(20);
|
||||
let cfg = $state(3.5);
|
||||
let shift = $state(5);
|
||||
let sampler = $state("euler");
|
||||
let scheduler = $state("simple");
|
||||
let seed = $state(0);
|
||||
let seedFixed = $state(false);
|
||||
|
||||
let promptText = $state(""); // mirror of the editor (fallback when no editor)
|
||||
let promptEditorView = null; // CM6 EditorView; authoritative prompt source once mounted
|
||||
|
||||
function seedSharedDefaults() {
|
||||
if (!defaults) return;
|
||||
width = defaults.width; height = defaults.height;
|
||||
length = defaults.length;
|
||||
outFps = 16; // the continuation is always i2v-native 16fps; stitch matches the source automatically
|
||||
}
|
||||
function seedRecipeDefaults(r) {
|
||||
const rd = recipeDefaults?.[r];
|
||||
if (!rd) return;
|
||||
steps = rd.steps; cfg = rd.cfg; shift = rd.shift;
|
||||
sampler = rd.sampler; scheduler = rd.scheduler;
|
||||
}
|
||||
|
||||
let primed = false;
|
||||
let lastRecipe = "normal";
|
||||
$effect(() => {
|
||||
if (open && !primed) {
|
||||
recipe = "normal"; lastRecipe = "normal"; stitch = true; advancedOpen = false;
|
||||
seed = 0; seedFixed = false; promptText = "";
|
||||
seedSharedDefaults(); seedRecipeDefaults("normal");
|
||||
primed = true;
|
||||
}
|
||||
if (!open) primed = false;
|
||||
});
|
||||
// Never leave Lightning selected once its LoRAs are known absent.
|
||||
$effect(() => { if (!lightningAvailable && recipe === "lightning") recipe = "normal"; });
|
||||
// Re-seed the sampler knobs when the recipe changes (lightning = 4-step / cfg 1).
|
||||
$effect(() => {
|
||||
if (recipe !== lastRecipe) { lastRecipe = recipe; seedRecipeDefaults(recipe); }
|
||||
});
|
||||
|
||||
let rd = $derived(recipeDefaults?.[recipe] || {});
|
||||
|
||||
let phase = $derived(progress?.phase || null);
|
||||
let running = $derived(phase === "building" || phase === "queueing" || phase === "running");
|
||||
let done = $derived(phase === "done");
|
||||
let pct = $derived(progress?.max ? Math.round((progress.value / progress.max) * 100) : 0);
|
||||
|
||||
const editorModelInfo = () => modelInfo || null;
|
||||
function prefill() { return `\nNegative Prompt: ${WAN_NEG}`; }
|
||||
|
||||
// Same use:action pattern as InpaintModal — mounts the CM6 editor while the
|
||||
// modal is open, mirrors its text into promptText, and tears down on close.
|
||||
function promptEditor(node) {
|
||||
let disposed = false, view = null;
|
||||
(async () => {
|
||||
if (!mountPromptEditor) return;
|
||||
const v = await mountPromptEditor(node, prefill(), (text) => { promptText = text; }, editorModelInfo);
|
||||
if (disposed) { v?.destroy?.(); return; }
|
||||
view = v; promptEditorView = v;
|
||||
})().catch((e) => console.error("[Extend] prompt editor mount failed", e));
|
||||
return { destroy() { disposed = true; view?.destroy?.(); if (promptEditorView === view) promptEditorView = null; } };
|
||||
}
|
||||
|
||||
function currentPromptDoc() {
|
||||
if (promptEditorView) return promptEditorView.state.doc.toString();
|
||||
const neg = `Negative Prompt: ${WAN_NEG}`;
|
||||
return promptText.trim() ? `${promptText.trim()}\n\n${neg}` : neg;
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (running) return;
|
||||
onRun?.({
|
||||
promptDoc: currentPromptDoc(),
|
||||
stitch, recipe,
|
||||
width, height, length, outFps,
|
||||
steps, cfg, shift, sampler, scheduler,
|
||||
seed: seedFixed ? seed : undefined, // omit unless pinned — keeps template noise behaviour
|
||||
});
|
||||
}
|
||||
|
||||
// Closing always lands on the latest result if one was rendered.
|
||||
function close() { if (done) onDone?.(); else onCancel?.(); }
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="pcr-ext-overlay" onclick={(e) => { if (e.target === e.currentTarget && !running) close(); }}>
|
||||
<div class="pcr-ext-modal">
|
||||
<div class="pcr-ext-title">Extend Video</div>
|
||||
<div class="pcr-ext-sub">
|
||||
Renders the next clip continuing from this video's last frame{frameCount ? ` · ${frameCount} frames @ ${fps}fps` : ""}. Saved as a lineage child.
|
||||
</div>
|
||||
|
||||
{#if lastFrameUrl}
|
||||
<div class="pcr-ext-frame">
|
||||
<img src={lastFrameUrl} alt="last frame" />
|
||||
<span>continues from here →</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pcr-ext-label">Speed</div>
|
||||
<div class="pcr-ext-recipe">
|
||||
<button
|
||||
class="pcr-ext-recipe-btn"
|
||||
class:active={recipe === "normal"}
|
||||
disabled={running}
|
||||
onclick={() => { recipe = "normal"; }}
|
||||
>
|
||||
<span class="pcr-ext-recipe-name">Normal <span class="pcr-ext-rec-dot" title="Recommended">●</span></span>
|
||||
<span class="pcr-ext-recipe-sub">20-step · best quality</span>
|
||||
</button>
|
||||
<button
|
||||
class="pcr-ext-recipe-btn"
|
||||
class:active={recipe === "lightning"}
|
||||
class:unavail={!lightningAvailable}
|
||||
disabled={running || !lightningAvailable}
|
||||
title={lightningAvailable ? "" : "Install the LightX2V speed LoRAs (~1.3 GB) to use Lightning"}
|
||||
onclick={() => { if (lightningAvailable) recipe = "lightning"; }}
|
||||
>
|
||||
<span class="pcr-ext-recipe-name">Lightning</span>
|
||||
<span class="pcr-ext-recipe-sub">{lightningAvailable ? "4-step · fast draft" : "speed LoRAs not installed"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="pcr-ext-label" for="pcr-ext-prompt">What happens next (motion / action)</label>
|
||||
{#if mountPromptEditor}
|
||||
<div class="pcr-ext-prompt pcr-ext-prompt-editor" use:promptEditor></div>
|
||||
{:else}
|
||||
<textarea
|
||||
id="pcr-ext-prompt"
|
||||
class="pcr-ext-prompt"
|
||||
bind:value={promptText}
|
||||
placeholder="e.g. the camera slowly pushes in as she turns her head and smiles"
|
||||
disabled={running}
|
||||
></textarea>
|
||||
{/if}
|
||||
|
||||
<label class="pcr-ext-stitch">
|
||||
<input type="checkbox" bind:checked={stitch} disabled={running} />
|
||||
Stitch into one continuous video (source + continuation)
|
||||
</label>
|
||||
|
||||
<!-- Advanced: sophisticated knobs, collapsed by default (template-correct
|
||||
defaults; the green ● marks the recipe's recommended value). -->
|
||||
<button class="pcr-ext-adv-toggle" onclick={() => advancedOpen = !advancedOpen}>
|
||||
{advancedOpen ? "▾" : "▸"} Advanced
|
||||
</button>
|
||||
{#if advancedOpen}
|
||||
<div class="pcr-ext-adv">
|
||||
<div class="pcr-ext-row">
|
||||
<span class="pcr-ext-row-label">Length <small>{length} frames</small></span>
|
||||
<SettingsSlider min={5} max={161} step={4} value={length} savedValue={defaults?.length}
|
||||
onChange={(v) => length = v} />
|
||||
</div>
|
||||
<div class="pcr-ext-row">
|
||||
<span class="pcr-ext-row-label">Output FPS <small>{outFps}</small></span>
|
||||
<SettingsSlider min={8} max={60} step={1} value={outFps} savedValue={16}
|
||||
onChange={(v) => outFps = v} />
|
||||
</div>
|
||||
<div class="pcr-ext-row">
|
||||
<span class="pcr-ext-row-label">Steps <small>{steps}</small></span>
|
||||
<SettingsSlider min={1} max={40} step={1} value={steps} savedValue={rd.steps}
|
||||
onChange={(v) => steps = v} />
|
||||
</div>
|
||||
<div class="pcr-ext-row">
|
||||
<span class="pcr-ext-row-label">CFG <small>{cfg}</small></span>
|
||||
<SettingsSlider min={1} max={8} step={0.1} value={cfg} savedValue={rd.cfg}
|
||||
onChange={(v) => cfg = v} />
|
||||
</div>
|
||||
<div class="pcr-ext-row">
|
||||
<span class="pcr-ext-row-label">Shift <small>{shift}</small></span>
|
||||
<SettingsSlider min={1} max={12} step={0.5} value={shift} savedValue={rd.shift}
|
||||
onChange={(v) => shift = v} />
|
||||
</div>
|
||||
<div class="pcr-ext-grid2">
|
||||
<label class="pcr-ext-field">
|
||||
<span>Sampler</span>
|
||||
<select bind:value={sampler} disabled={running}>
|
||||
{#each SAMPLERS as s}<option value={s}>{s}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="pcr-ext-field">
|
||||
<span>Scheduler</span>
|
||||
<select bind:value={scheduler} disabled={running}>
|
||||
{#each SCHEDULERS as s}<option value={s}>{s}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pcr-ext-grid2">
|
||||
<label class="pcr-ext-field">
|
||||
<span>Seed</span>
|
||||
<input type="number" bind:value={seed} disabled={running || !seedFixed} />
|
||||
</label>
|
||||
<label class="pcr-ext-field pcr-ext-field-check">
|
||||
<input type="checkbox" bind:checked={seedFixed} disabled={running} />
|
||||
<span>Fix seed</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if progress}
|
||||
<div class="pcr-ext-progress">
|
||||
{#if phase === "error"}
|
||||
<div class="pcr-ext-err">⚠ {progress.message || "render failed"}</div>
|
||||
{:else if done}
|
||||
<div class="pcr-ext-done">✓ Continuation rendered. Adjust and Run Again, or Done to view it.</div>
|
||||
{#if progress.resultUrl}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video class="pcr-ext-result" src={progress.resultUrl} controls autoplay loop muted></video>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="pcr-ext-bar"><div class="pcr-ext-fill" style="width:{pct}%"></div></div>
|
||||
<div class="pcr-ext-phase">{phase}{progress.max ? ` · ${pct}%` : "…"}</div>
|
||||
{#if progress.previewUrl}
|
||||
<img class="pcr-ext-preview" src={progress.previewUrl} alt="preview" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pcr-ext-actions">
|
||||
{#if running}
|
||||
<button class="pcr-ext-btn" onclick={() => onCancel?.()}>Cancel</button>
|
||||
{:else if done}
|
||||
<button class="pcr-ext-btn" onclick={() => onDone?.()}>Done</button>
|
||||
<button class="pcr-ext-btn pcr-ext-primary" onclick={run}>Run Again</button>
|
||||
{:else}
|
||||
<button class="pcr-ext-btn" onclick={() => onCancel?.()}>Close</button>
|
||||
<button class="pcr-ext-btn pcr-ext-primary" onclick={run}>Extend</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.pcr-ext-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 10011;
|
||||
}
|
||||
.pcr-ext-modal {
|
||||
background: #1e1e1e; border: 1px solid #444; border-radius: 8px;
|
||||
padding: 20px 24px; min-width: 380px; max-width: 480px;
|
||||
max-height: 88vh; overflow-y: auto;
|
||||
}
|
||||
.pcr-ext-title { font-size: 15px; font-weight: 600; color: #eee; margin-bottom: 4px; }
|
||||
.pcr-ext-sub { font-size: 11px; color: #888; margin-bottom: 14px; line-height: 1.4; }
|
||||
.pcr-ext-frame { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.pcr-ext-frame img { width: 96px; height: 96px; object-fit: cover; border-radius: 4px; border: 1px solid #333; }
|
||||
.pcr-ext-frame span { font-size: 11px; color: #777; }
|
||||
.pcr-ext-label { display: block; font-size: 11px; color: #aaa; margin-bottom: 5px; }
|
||||
.pcr-ext-recipe { display: flex; gap: 8px; margin-bottom: 14px; }
|
||||
.pcr-ext-recipe-btn {
|
||||
flex: 1; display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
|
||||
padding: 8px 10px; background: #161616; border: 1px solid #444; border-radius: 4px;
|
||||
color: #ddd; cursor: pointer; text-align: left; transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.pcr-ext-recipe-btn:hover:not(:disabled) { border-color: #666; }
|
||||
.pcr-ext-recipe-btn.active { border-color: #4fc3f7; background: #15242b; }
|
||||
.pcr-ext-recipe-btn.unavail { opacity: 0.5; cursor: not-allowed; }
|
||||
.pcr-ext-recipe-btn:disabled { cursor: default; }
|
||||
.pcr-ext-recipe-name { font-size: 12px; font-weight: 600; }
|
||||
.pcr-ext-recipe-sub { font-size: 10px; color: #888; }
|
||||
.pcr-ext-rec-dot { color: #4caf50; font-size: 9px; vertical-align: middle; }
|
||||
.pcr-ext-prompt {
|
||||
width: 100%; box-sizing: border-box; min-height: 70px; resize: vertical;
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 8px; font-family: inherit;
|
||||
}
|
||||
.pcr-ext-prompt:focus { outline: none; border-color: #4fc3f7; }
|
||||
.pcr-ext-prompt-editor { padding: 0; overflow: hidden; min-height: 84px; resize: none; }
|
||||
.pcr-ext-stitch { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #aaa; margin-top: 10px; cursor: pointer; }
|
||||
.pcr-ext-adv-toggle {
|
||||
margin-top: 12px; background: none; border: none; color: #8bd5f5; font-size: 11px;
|
||||
cursor: pointer; padding: 2px 0;
|
||||
}
|
||||
.pcr-ext-adv { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.pcr-ext-row { display: flex; flex-direction: column; gap: 4px; }
|
||||
.pcr-ext-row-label { font-size: 11px; color: #aaa; display: flex; justify-content: space-between; }
|
||||
.pcr-ext-row-label small { color: #6cc; }
|
||||
.pcr-ext-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.pcr-ext-field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #aaa; }
|
||||
.pcr-ext-field select, .pcr-ext-field input[type="number"] {
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 5px 6px; font-family: inherit;
|
||||
}
|
||||
.pcr-ext-field-check { flex-direction: row; align-items: center; gap: 6px; align-self: end; padding-bottom: 6px; }
|
||||
.pcr-ext-progress { margin-top: 12px; }
|
||||
.pcr-ext-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
||||
.pcr-ext-fill { height: 100%; background: #4fc3f7; border-radius: 3px; transition: width 0.3s; }
|
||||
.pcr-ext-phase { font-size: 11px; color: #aaa; margin-top: 6px; text-transform: capitalize; }
|
||||
.pcr-ext-preview, .pcr-ext-result { width: 100%; margin-top: 8px; border-radius: 4px; max-height: 240px; }
|
||||
.pcr-ext-err { font-size: 12px; color: #f44336; }
|
||||
.pcr-ext-done { font-size: 12px; color: #4caf50; margin-bottom: 6px; }
|
||||
.pcr-ext-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.pcr-ext-btn {
|
||||
padding: 6px 16px; font-size: 12px; border: 1px solid #555; border-radius: 4px;
|
||||
background: transparent; color: #ccc; cursor: pointer;
|
||||
}
|
||||
.pcr-ext-btn:hover { background: rgba(255,255,255,0.08); }
|
||||
.pcr-ext-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.pcr-ext-primary { background: #4fc3f7; color: #111; border-color: #4fc3f7; }
|
||||
.pcr-ext-primary:hover { background: #39b0e4; }
|
||||
.pcr-ext-primary:disabled { background: #2a7a9e; }
|
||||
</style>
|
||||
@@ -4,6 +4,8 @@
|
||||
import InpaintModal from "./InpaintModal.svelte";
|
||||
import EditModal from "./EditModal.svelte";
|
||||
import RePoseModal from "./RePoseModal.svelte";
|
||||
import ExtendModal from "./ExtendModal.svelte";
|
||||
import SmoothModal from "./SmoothModal.svelte";
|
||||
import ConfirmModal from "./sidebar/ConfirmModal.svelte";
|
||||
import LineageContextMenu from "./LineageContextMenu.svelte";
|
||||
import { buildRender, ancestry, CHILDREN_PER_ROW_CAP, CHILDREN_REVEAL_STEP } from "../lib/lineage-lanes.js";
|
||||
@@ -33,6 +35,12 @@
|
||||
onReposeCaps = null, // () => Promise<{recipes}> — installable pose-transfer recipes
|
||||
onReposeRun = null, // (opts) => tracker — background pose-transfer render
|
||||
onMountPoser = null, // (el, {width,height,outputMode}) => Promise<handle> — detached 3D poser
|
||||
onExtendPrepare = null, // (hash) => Promise<{lastFrameFilename,sourceRef,fps,frameCount,templateId,...}>
|
||||
onExtendRun = null, // (opts) => tracker — background video continuation render
|
||||
onSmoothCheck = null, // () => bool — RIFE pack + core video nodes registered
|
||||
onSmoothInstall = null, // () => Promise — offer to install the RIFE pack
|
||||
onSmoothPrepare = null, // (hash) => Promise<{sourceRef,fps,frameCount,...}>
|
||||
onSmoothRun = null, // (opts) => tracker — background RIFE interpolation
|
||||
autoEdit = false,
|
||||
} = $props();
|
||||
|
||||
@@ -387,6 +395,14 @@
|
||||
// Per-parent override of how many siblings render individually before the rest
|
||||
// fold into a "+N" bundle chip; bumped by CHILDREN_REVEAL_STEP on a chip click.
|
||||
let lineageRevealed = $state(new Map());
|
||||
// Hashes the user pinned: a pinned sibling never folds into a +N bundle. Persisted
|
||||
// per family alongside the expansion overlay.
|
||||
let pinnedHashes = $state(new Set());
|
||||
function togglePin(hash) {
|
||||
const next = new Set(pinnedHashes);
|
||||
if (next.has(hash)) next.delete(hash); else next.add(hash);
|
||||
pinnedHashes = next;
|
||||
}
|
||||
let subwayAnchor = $derived(
|
||||
lineageFocusHash && lineageTree.byHash.has(lineageFocusHash) ? lineageFocusHash : displayedHash
|
||||
);
|
||||
@@ -394,7 +410,7 @@
|
||||
// subtree (no parent_hash mutation, no effect on which image is displayed/loaded).
|
||||
let focusRootHash = $state(null);
|
||||
let laneRender = $derived(buildRender(lineageTree, subwayAnchor, brokenOut,
|
||||
{ displayedHash, revealed: lineageRevealed, rootHash: focusRootHash }));
|
||||
{ displayedHash, revealed: lineageRevealed, rootHash: focusRootHash, pinned: pinnedHashes }));
|
||||
let lanesActive = $derived((brokenOut.size > 0 || !!focusRootHash) && lineageList.length > 1);
|
||||
// is `desc` the focus root or a descendant of it? (guarded parent_hash climb)
|
||||
function isInSubtree(desc, rootHash) {
|
||||
@@ -448,15 +464,16 @@
|
||||
const prefs = readLanePrefs()[root];
|
||||
brokenOut = new Set((prefs?.brokenOut || []).filter(h => lineageTree.byHash.has(h)));
|
||||
lineageRevealed = new Map((prefs?.revealed || []).filter(([k]) => lineageTree.byHash.has(k)));
|
||||
pinnedHashes = new Set((prefs?.pinned || []).filter(h => lineageTree.byHash.has(h)));
|
||||
});
|
||||
// persist after hydration; an empty overlay drops the entry rather than storing {}
|
||||
$effect(() => {
|
||||
const root = familyRootHash;
|
||||
const bo = [...brokenOut], rv = [...lineageRevealed];
|
||||
const bo = [...brokenOut], rv = [...lineageRevealed], pn = [...pinnedHashes];
|
||||
if (!root || root !== hydratedRoot) return;
|
||||
try {
|
||||
const all = readLanePrefs();
|
||||
if (bo.length || rv.length) all[root] = { brokenOut: bo, revealed: rv };
|
||||
if (bo.length || rv.length || pn.length) all[root] = { brokenOut: bo, revealed: rv, pinned: pn };
|
||||
else delete all[root];
|
||||
localStorage.setItem(LANE_PREFS_KEY, JSON.stringify(all));
|
||||
} catch {}
|
||||
@@ -517,7 +534,9 @@
|
||||
// a plain click clears any selection and navigates.
|
||||
function handleNodeClick(e, item) {
|
||||
if (compareDropdownOpen) {
|
||||
if (item.hash !== displayedHash) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
|
||||
// mirror the dropdown's video filter — a video can't be the clip-path
|
||||
// "after" frame, so picking one would paint raw bytes into an <img>
|
||||
if (item.hash !== displayedHash && !isVideoItem(item)) enterCompareWith(item.hash, item.filename || item.hash.slice(0, 8));
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey) { toggleSelect(item.hash); return; }
|
||||
@@ -692,7 +711,9 @@
|
||||
});
|
||||
|
||||
let compareTargets = $derived(
|
||||
lineageList.filter(item => item?.hash !== displayedHash)
|
||||
// wipe-compare only makes sense between two stills — a video can't be the
|
||||
// clip-path "after" frame, so it never appears as a compare target
|
||||
lineageList.filter(item => item?.hash !== displayedHash && !isVideoItem(item))
|
||||
);
|
||||
let hasCompareTargets = $derived(compareTargets.length > 0);
|
||||
let compareImageUrl = $derived(compareTargetHash ? imageUrl(compareTargetHash) : null);
|
||||
@@ -837,6 +858,45 @@
|
||||
if (img?._directUrl) return img._directUrl;
|
||||
return apiURL(`/promptchain/thumb/${hash}`);
|
||||
}
|
||||
// A thumbnail <img> can latch onto a 404 from a request that raced the
|
||||
// server-side record (the thumb is written as part of recording); a broken
|
||||
// <img> never retries itself, so a transient miss leaves a lineage node blank
|
||||
// forever. Cache-bust and re-fetch on each error — the 404 is served no-store,
|
||||
// so the thumb is on disk by now. Bounded, mirrors the gallery's retry; no polling.
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
|
||||
// Video outputs (Wan/AnimateDiff/…) are served by /promptchain/image/<hash>
|
||||
// with their real mime type, but the hash-keyed URL carries no extension to
|
||||
// detect them by. The displayed item's filename — from the gallery entry, its
|
||||
// family node, or the fetched meta — is what tells us to render a <video>
|
||||
// instead of an <img>. Thumbnails already poster-frame videos server-side, so
|
||||
// the only signal a thumb is a clip is the ▶ badge keyed off this.
|
||||
// superset of every backend video set so detection can't drift below them:
|
||||
// browse_api _VIDEO_EXTS (…avi) and thumbs VIDEO_SUFFIXES (…mkv) both classify
|
||||
// formats this regex must also accept, or a browse-opened clip falls through to
|
||||
// a broken <img>.
|
||||
const VIDEO_EXT_RE = /\.(mp4|webm|m4v|mkv|mov|avi)(\?|$)/i;
|
||||
function isVideoItem(item) {
|
||||
return VIDEO_EXT_RE.test(item?.filename || "");
|
||||
}
|
||||
function isVideoHash(hash) {
|
||||
if (!hash) return false;
|
||||
const entry = imageByHash.get(hash);
|
||||
if (entry?.filename) return isVideoItem(entry);
|
||||
const fam = lineageList.find((i) => i?.hash === hash);
|
||||
if (fam?.filename) return isVideoItem(fam);
|
||||
if (imageInfo?.hash === hash) return isVideoItem(imageInfo);
|
||||
return false;
|
||||
}
|
||||
let displayedIsVideo = $derived(isVideoHash(displayedHash));
|
||||
|
||||
// timeline navigation — moves scrubber position and syncs displayed image
|
||||
// zoom/pan is preserved across navigation (only resets on double-click or first open)
|
||||
@@ -853,8 +913,26 @@
|
||||
|
||||
// lineage navigation — only changes displayed image, not timeline position
|
||||
function navigateLineage(delta) {
|
||||
// walk the VISIBLE strip only — collapsed bundles are skipped; expanding
|
||||
// one brings its branch into the walk
|
||||
// In the expanded tree, Up/Down move by GENERATION among the RENDERED nodes
|
||||
// (parent / primary child), not the flat strip order — which can contain nodes
|
||||
// the tree doesn't show.
|
||||
if (lanesActive) {
|
||||
const renderedReal = (h) => laneRender.nodes.some(n => n.kind === "node" && n.hash === h);
|
||||
if (delta < 0) {
|
||||
const p = lineageTree.byHash.get(displayedHash)?.parent_hash;
|
||||
if (p && renderedReal(p)) { lineageJump(p); exitCompare(); }
|
||||
} else {
|
||||
const kids = (lineageTree.childrenOf.get(displayedHash) || []).map(k => k.hash).filter(renderedReal);
|
||||
if (kids.length) {
|
||||
const a = ancestry(lineageTree, subwayAnchor);
|
||||
lineageJump(kids.find(h => a.set.has(h)) || kids[0]);
|
||||
exitCompare();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// strip mode: walk the VISIBLE strip only — collapsed bundles are skipped;
|
||||
// expanding one brings its branch into the walk
|
||||
const idx = lineageVisible.findIndex(item => item.hash === displayedHash);
|
||||
if (idx < 0) return;
|
||||
const next = idx + delta;
|
||||
@@ -864,8 +942,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Left/Right in the expanded tree hop between siblings at the same depth (by column).
|
||||
function navigateTreeSibling(delta) {
|
||||
const reals = laneRender.nodes.filter(n => n.kind === "node");
|
||||
const cur = reals.find(n => n.hash === displayedHash);
|
||||
if (!cur) return;
|
||||
const row = reals.filter(n => n.depth === cur.depth).sort((a, b) => a.x - b.x);
|
||||
const i = row.findIndex(n => n.hash === displayedHash);
|
||||
const next = row[i + delta];
|
||||
if (next) { lineageJump(next.hash); exitCompare(); }
|
||||
}
|
||||
|
||||
// compare mode
|
||||
function enterCompareWith(targetHash, label) {
|
||||
if (isVideoHash(targetHash)) return; // a video can't be the wipe "after" frame
|
||||
if (compareMode && compareTargetHash === targetHash) { exitCompare(); return; }
|
||||
const fresh = !compareMode;
|
||||
compareTargetHash = targetHash;
|
||||
@@ -888,6 +978,13 @@
|
||||
compareDropdownOpen = false;
|
||||
}
|
||||
|
||||
// Navigating onto a video (e.g. up/down the family) drops out of compare —
|
||||
// the wipe overlay paints an <img> that can't show a clip. Writes only when
|
||||
// already comparing, so it can't loop.
|
||||
$effect(() => {
|
||||
if (displayedIsVideo && (compareMode || compareDropdownOpen)) exitCompare();
|
||||
});
|
||||
|
||||
function handleSliderDown(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1181,6 +1278,168 @@
|
||||
reposeTracker = null;
|
||||
}
|
||||
|
||||
// ── extend (video continuation — viewer post-process for video clips) ──
|
||||
let extendModalOpen = $state(false);
|
||||
let extendPrepared = $state(null); // { lastFrameFilename, lastFrameUrl, sourceRef, fps, frameCount, templateId, defaults, recipeDefaults, lightningAvailable }
|
||||
let extendProgress = $state(null);
|
||||
let extendPreparing = $state(false);
|
||||
let extendLastEntry = $state(null); // result of the latest run, for the "Done" jump
|
||||
let extendTracker = null, extendUnsub = null;
|
||||
|
||||
// Splice a freshly-rendered result into the timeline and navigate to it, so the
|
||||
// user lands on the new clip when processing finishes and a follow-up action
|
||||
// (Extend/Smooth) runs on it — not the source. Mirrors inpaintSaved.
|
||||
function landOnResult(entry) {
|
||||
if (!entry?.hash) return;
|
||||
const existingIdx = images.findIndex((i) => i.hash === entry.hash);
|
||||
if (existingIdx >= 0) {
|
||||
currentIndex = existingIdx;
|
||||
} else {
|
||||
const srcIdx = images.findIndex((i) => i.hash === displayedHash);
|
||||
const insertAt = srcIdx >= 0 ? srcIdx + 1 : images.length;
|
||||
images = [...images.slice(0, insertAt), entry, ...images.slice(insertAt)];
|
||||
currentIndex = insertAt;
|
||||
}
|
||||
displayedHash = entry.hash;
|
||||
}
|
||||
|
||||
async function openExtend() {
|
||||
if (extendPreparing || !onExtendPrepare || !onExtendRun || !displayedHash) return;
|
||||
extendPreparing = true;
|
||||
try {
|
||||
const prepared = await onExtendPrepare(displayedHash);
|
||||
if (prepared?.lastFrameFilename) {
|
||||
extendPrepared = prepared;
|
||||
extendProgress = null;
|
||||
extendModalOpen = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[PromptChain] extend prepare failed", e);
|
||||
}
|
||||
extendPreparing = false;
|
||||
}
|
||||
|
||||
function confirmExtend(opts) {
|
||||
if (!onExtendRun || !extendPrepared) return;
|
||||
// Recipe = which template renders the continuation. "lightning" swaps to the
|
||||
// "-fast" sibling (4-step distill LoRAs, baked correctly); "normal" keeps the
|
||||
// vanilla base. All the other fields are optional overrides buildExtendGraph
|
||||
// applies on top of the template (omitted => template default).
|
||||
const base = extendPrepared.templateId;
|
||||
const templateId = opts.recipe === "lightning" ? `${base}-fast` : base;
|
||||
const tracker = onExtendRun({
|
||||
templateId,
|
||||
lastFrameFilename: extendPrepared.lastFrameFilename,
|
||||
parentFilename: extendPrepared.sourceRef,
|
||||
length: Math.min(161, opts.length || extendPrepared.frameCount || 81), // ceiling: one continuation clip, never a long source's full frame count
|
||||
width: opts.width || undefined,
|
||||
height: opts.height || undefined,
|
||||
outFps: opts.outFps || undefined,
|
||||
steps: opts.steps, cfg: opts.cfg, shift: opts.shift,
|
||||
sampler: opts.sampler, scheduler: opts.scheduler,
|
||||
seed: opts.seed, // undefined unless the user pinned one
|
||||
promptDoc: opts.promptDoc,
|
||||
stitch: opts.stitch,
|
||||
sourceVideoFilename: extendPrepared.sourceRef,
|
||||
fps: extendPrepared.fps,
|
||||
});
|
||||
extendTracker = tracker;
|
||||
extendUnsub?.();
|
||||
extendUnsub = tracker.subscribe((state) => {
|
||||
if (state.phase === "cancelled") { extendProgress = null; return; }
|
||||
extendProgress = state;
|
||||
// Keep the modal open on done so the user can compare, tweak settings, and
|
||||
// Run Again; remember the result so "Done" can jump to it (finishExtend).
|
||||
if (state.phase === "done" && state.entry?.hash) extendLastEntry = state.entry;
|
||||
});
|
||||
}
|
||||
|
||||
function finishExtend() {
|
||||
if (extendLastEntry) { landOnResult(extendLastEntry); showNewGenBanner(null); }
|
||||
closeExtend();
|
||||
}
|
||||
|
||||
function closeExtend() {
|
||||
extendModalOpen = false;
|
||||
extendPrepared = null;
|
||||
extendProgress = null;
|
||||
extendLastEntry = null;
|
||||
extendUnsub?.();
|
||||
extendUnsub = null;
|
||||
extendTracker = null;
|
||||
}
|
||||
|
||||
// ── smooth (RIFE interpolation — viewer post-process for video clips) ──
|
||||
let smoothModalOpen = $state(false);
|
||||
let smoothPrepared = $state(null); // { sourceRef, fps, frameCount }
|
||||
let smoothProgress = $state(null);
|
||||
let smoothPreparing = $state(false);
|
||||
let smoothLastEntry = $state(null); // result of the latest run, for the "Done" jump
|
||||
let smoothTracker = null, smoothUnsub = null;
|
||||
|
||||
async function openSmooth() {
|
||||
if (smoothPreparing || !onSmoothPrepare || !onSmoothRun || !displayedHash) return;
|
||||
// Gate on the RIFE pack: offer to install it on first use, then bail (the
|
||||
// user re-opens once it's registered). Same contract as the inpaint packs.
|
||||
if (onSmoothCheck && !onSmoothCheck()) { onSmoothInstall?.(); return; }
|
||||
smoothPreparing = true;
|
||||
try {
|
||||
const prepared = await onSmoothPrepare(displayedHash);
|
||||
if (prepared?.sourceRef) {
|
||||
smoothPrepared = prepared;
|
||||
smoothProgress = null;
|
||||
smoothModalOpen = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[PromptChain] smooth prepare failed", e);
|
||||
}
|
||||
smoothPreparing = false;
|
||||
}
|
||||
|
||||
function confirmSmooth(opts) {
|
||||
if (!onSmoothRun || !smoothPrepared) return;
|
||||
// CreateVideo caps fps at 120, and ComfyUI REJECTS (not clamps) over-max — so
|
||||
// smoothing an already-high-fps clip (32×4=128) would hard-fail the queue.
|
||||
// Cap the multiplier so source_fps × mult never exceeds 120; keeps fps and
|
||||
// frame count in sync (vs clamping outFps alone, which desyncs playback speed).
|
||||
const srcFps = smoothPrepared.fps || 16;
|
||||
const maxMult = Math.max(2, Math.floor(120 / srcFps));
|
||||
const mult = Math.min(opts.multiplier || 2, maxMult);
|
||||
const tracker = onSmoothRun({
|
||||
sourceVideoFilename: smoothPrepared.sourceRef,
|
||||
multiplier: mult,
|
||||
outFps: Math.round(srcFps * mult),
|
||||
ckptName: opts.ckptName,
|
||||
scaleFactor: opts.scaleFactor,
|
||||
ensemble: opts.ensemble,
|
||||
fastMode: opts.fastMode,
|
||||
parentFilename: smoothPrepared.sourceRef,
|
||||
});
|
||||
smoothTracker = tracker;
|
||||
smoothUnsub?.();
|
||||
smoothUnsub = tracker.subscribe((state) => {
|
||||
if (state.phase === "cancelled") { smoothProgress = null; return; }
|
||||
smoothProgress = state;
|
||||
// Stay open on done for compare / Run Again; remember the result for "Done".
|
||||
if (state.phase === "done" && state.entry?.hash) smoothLastEntry = state.entry;
|
||||
});
|
||||
}
|
||||
|
||||
function finishSmooth() {
|
||||
if (smoothLastEntry) { landOnResult(smoothLastEntry); showNewGenBanner(null); }
|
||||
closeSmooth();
|
||||
}
|
||||
|
||||
function closeSmooth() {
|
||||
smoothModalOpen = false;
|
||||
smoothPrepared = null;
|
||||
smoothProgress = null;
|
||||
smoothLastEntry = null;
|
||||
smoothUnsub?.();
|
||||
smoothUnsub = null;
|
||||
smoothTracker = null;
|
||||
}
|
||||
|
||||
// ── inpaint ──
|
||||
let inpaintModalOpen = $state(false);
|
||||
let inpaintPrepared = $state(null);
|
||||
@@ -1248,6 +1507,7 @@
|
||||
|
||||
async function openEdit(targetHash = displayedHash) {
|
||||
if (editPreparing || !onEditPrepare || !onEditSave) return;
|
||||
if (isVideoHash(targetHash)) return; // the editor can't open a video clip
|
||||
const entry = images.find((i) => i.hash === targetHash) || { hash: targetHash };
|
||||
editPreparing = true;
|
||||
try {
|
||||
@@ -1522,14 +1782,16 @@
|
||||
requestDeleteBatch();
|
||||
return;
|
||||
}
|
||||
if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && hasCompareTargets) {
|
||||
if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && hasCompareTargets && !displayedIsVideo) {
|
||||
e.preventDefault();
|
||||
if (compareMode) exitCompare();
|
||||
else compareDropdownOpen = !compareDropdownOpen;
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowLeft") { navigate(-1); return; }
|
||||
if (e.key === "ArrowRight") { navigate(1); return; }
|
||||
// In the expanded tree the arrows navigate the 2D family (Up/Down = generation,
|
||||
// Left/Right = siblings); in the strip, Left/Right stay on the timeline.
|
||||
if (e.key === "ArrowLeft") { if (lanesActive) { e.preventDefault(); navigateTreeSibling(-1); } else navigate(-1); return; }
|
||||
if (e.key === "ArrowRight") { if (lanesActive) { e.preventDefault(); navigateTreeSibling(1); } else navigate(1); return; }
|
||||
if (e.key === "ArrowUp") { e.preventDefault(); navigateLineage(-1); return; }
|
||||
if (e.key === "ArrowDown") { e.preventDefault(); navigateLineage(1); return; }
|
||||
}
|
||||
@@ -1715,6 +1977,8 @@
|
||||
expandBranches(item);
|
||||
} else if (action === "focus-branch") {
|
||||
focusBranch(hash);
|
||||
} else if (action === "pin") {
|
||||
togglePin(hash);
|
||||
} else if (action === "copy-path") {
|
||||
copyPath(hash);
|
||||
} else if (action === "edit") {
|
||||
@@ -1900,8 +2164,9 @@
|
||||
data-hash={item.hash}
|
||||
class:current={item.hash === displayedHash}
|
||||
class:selected={selectedHashes.has(item.hash)}
|
||||
class:pinned={pinnedHashes.has(item.hash)}
|
||||
class:orphaned={item.orphaned === 1}
|
||||
class:compare-candidate={compareDropdownOpen && item.hash !== displayedHash}
|
||||
class:compare-candidate={compareDropdownOpen && item.hash !== displayedHash && !isVideoItem(item)}
|
||||
class:compare-left={compareMode && item.hash === displayedHash}
|
||||
class:compare-right={compareMode && item.hash === compareTargetHash}
|
||||
onclick={(e) => handleNodeClick(e, item)}
|
||||
@@ -1909,8 +2174,10 @@
|
||||
onmouseleave={hideTip}
|
||||
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
|
||||
>
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{#if isVideoItem(item)}<div class="pcr-viewer-vidbadge" aria-hidden="true">▶</div>{/if}
|
||||
{#if selectedHashes.has(item.hash)}<div class="pcr-lin-selcheck">✓</div>{/if}
|
||||
{#if pinnedHashes.has(item.hash)}<div class="pcr-lin-pinbadge" title="Pinned — kept out of bundles">★</div>{/if}
|
||||
<div class="pcr-viewer-lineage-dot"
|
||||
class:dot-root={item.hash === laneRender.rootHash}
|
||||
class:dot-current={item.hash === displayedHash}
|
||||
@@ -1987,7 +2254,7 @@
|
||||
onclick={() => revealMore(node.parent)}>
|
||||
<div class="pcr-viewer-lineage-bundle-stack">
|
||||
{#each node.preview as p (p)}
|
||||
<img src={thumbUrl(p)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(p)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{/each}
|
||||
</div>
|
||||
<span class="pcr-viewer-lineage-bundle-count">+{node.count}</span>
|
||||
@@ -2013,8 +2280,9 @@
|
||||
class:descendant={i > lineageDisplayCurrentIdx}
|
||||
class:branch={d.branch}
|
||||
class:selected={selectedHashes.has(item.hash)}
|
||||
class:pinned={pinnedHashes.has(item.hash)}
|
||||
class:orphaned={item.orphaned === 1}
|
||||
class:compare-candidate={compareDropdownOpen && item.hash !== displayedHash}
|
||||
class:compare-candidate={compareDropdownOpen && item.hash !== displayedHash && !isVideoItem(item)}
|
||||
class:compare-left={compareMode && item.hash === displayedHash}
|
||||
class:compare-right={compareMode && item.hash === compareTargetHash}
|
||||
onclick={(e) => handleNodeClick(e, item)}
|
||||
@@ -2022,8 +2290,10 @@
|
||||
onmouseleave={hideTip}
|
||||
oncontextmenu={(e) => { hideTip(); openLineageMenu(e, item); }}
|
||||
>
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(item.hash)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{#if isVideoItem(item)}<div class="pcr-viewer-vidbadge" aria-hidden="true">▶</div>{/if}
|
||||
{#if selectedHashes.has(item.hash)}<div class="pcr-lin-selcheck">✓</div>{/if}
|
||||
{#if pinnedHashes.has(item.hash)}<div class="pcr-lin-pinbadge" title="Pinned — kept out of bundles">★</div>{/if}
|
||||
<div class="pcr-viewer-lineage-dot"
|
||||
class:dot-root={item.hash === lineageList[0]?.hash}
|
||||
class:dot-current={i === lineageDisplayCurrentIdx}
|
||||
@@ -2051,7 +2321,7 @@
|
||||
{#if d.kind === "bundle"}
|
||||
<div class="pcr-viewer-lineage-bundle-stack">
|
||||
{#each d.preview as p (p.hash)}
|
||||
<img src={thumbUrl(p.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(p.hash)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{/each}
|
||||
</div>
|
||||
<span class="pcr-viewer-lineage-bundle-count">+{d.count}</span>
|
||||
@@ -2109,17 +2379,42 @@
|
||||
{#if !imageLoaded && !imageError}
|
||||
<div class="pcr-viewer-spinner"></div>
|
||||
{/if}
|
||||
<img
|
||||
bind:this={mainImageEl}
|
||||
src={mainImageSrc(displayedHash)}
|
||||
alt=""
|
||||
draggable="false"
|
||||
class="pcr-viewer-image"
|
||||
class:hidden={!imageLoaded}
|
||||
style="transform: translate({panX}px, {panY}px) scale({zoom})"
|
||||
onload={() => { imageLoaded = true; }}
|
||||
onerror={() => { imageError = true; imageLoaded = false; }}
|
||||
/>
|
||||
{#if displayedIsVideo}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<!-- stopPropagation keeps the container's pan/gesture handler (which
|
||||
preventDefaults) from swallowing the native transport controls;
|
||||
wheel still bubbles up for zoom. poster shows the server frame
|
||||
while the clip buffers. -->
|
||||
<video
|
||||
bind:this={mainImageEl}
|
||||
src={mainImageSrc(displayedHash)}
|
||||
poster={imageByHash.get(displayedHash)?._directUrl ? null : thumbUrl(displayedHash)}
|
||||
class="pcr-viewer-image"
|
||||
class:hidden={!imageLoaded}
|
||||
style="transform: translate({panX}px, {panY}px) scale({zoom})"
|
||||
onloadeddata={() => { imageLoaded = true; }}
|
||||
onerror={() => { imageError = true; imageLoaded = false; }}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
ondblclick={(e) => e.stopPropagation()}
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
controls
|
||||
></video>
|
||||
{:else}
|
||||
<img
|
||||
bind:this={mainImageEl}
|
||||
src={mainImageSrc(displayedHash)}
|
||||
alt=""
|
||||
draggable="false"
|
||||
class="pcr-viewer-image"
|
||||
class:hidden={!imageLoaded}
|
||||
style="transform: translate({panX}px, {panY}px) scale({zoom})"
|
||||
onload={() => { imageLoaded = true; }}
|
||||
onerror={() => { imageError = true; imageLoaded = false; }}
|
||||
/>
|
||||
{/if}
|
||||
{#if compareMode && compareImageUrl}
|
||||
<img
|
||||
src={compareImageUrl}
|
||||
@@ -2215,7 +2510,8 @@
|
||||
class:current={thumb.globalIndex === currentIndex}
|
||||
onclick={() => jumpTo(thumb.globalIndex)}
|
||||
>
|
||||
<img src={thumbUrl(thumb.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(thumb.hash)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{#if isVideoItem(thumb)}<div class="pcr-viewer-vidbadge" aria-hidden="true">▶</div>{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="pcr-viewer-history-fade"></div>
|
||||
@@ -2256,7 +2552,8 @@
|
||||
class:current={idx === currentIndex}
|
||||
onclick={() => jumpTo(idx)}
|
||||
>
|
||||
<img src={thumbUrl(img.hash)} alt="" draggable="false" loading="lazy" />
|
||||
<img src={thumbUrl(img.hash)} alt="" draggable="false" loading="lazy" onerror={retryThumb} />
|
||||
{#if isVideoItem(img)}<div class="pcr-viewer-vidbadge" aria-hidden="true">▶</div>{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2292,7 +2589,7 @@
|
||||
</div>
|
||||
|
||||
<!-- compare row -->
|
||||
{#if hasCompareTargets}
|
||||
{#if hasCompareTargets && !displayedIsVideo}
|
||||
<div class="pcr-viewer-toolbar-row pcr-viewer-compare-container">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -2370,8 +2667,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- edit row -->
|
||||
{#if onEditSave}
|
||||
<!-- edit row — image-only: Edit/inpaint/upscale run on a still raster, so
|
||||
it's hidden for video clips (no meaningful editor handoff exists) -->
|
||||
{#if onEditSave && !displayedIsVideo}
|
||||
<div class="pcr-viewer-toolbar-row">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -2394,6 +2692,42 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- extend row — video clips only: render the next clip from the last frame -->
|
||||
{#if onExtendRun && displayedIsVideo}
|
||||
<div class="pcr-viewer-toolbar-row">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-toolbar-btn"
|
||||
onclick={() => { if (!extendPreparing) openExtend(); }}
|
||||
title="Render the next clip, continuing from this video's last frame"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="5 3 16 12 5 21 5 3"/><line x1="20" y1="4" x2="20" y2="20"/>
|
||||
</svg>
|
||||
<span>{extendPreparing ? "Reading..." : "Extend"}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- smooth row — video clips only: RIFE frame interpolation -->
|
||||
{#if onSmoothRun && displayedIsVideo}
|
||||
<div class="pcr-viewer-toolbar-row">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="pcr-viewer-toolbar-btn"
|
||||
onclick={() => { if (!smoothPreparing) openSmooth(); }}
|
||||
title="Interpolate extra frames (RIFE) for smoother, higher-fps motion"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 12h4l3-9 4 18 3-9h4"/>
|
||||
</svg>
|
||||
<span>{smoothPreparing ? "Reading..." : "Smooth"}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- copy link row -->
|
||||
<div class="pcr-viewer-toolbar-row pcr-viewer-toolbar-row-equal">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
@@ -2642,6 +2976,31 @@
|
||||
onCancel={() => { if (reposeProgress && ["building","queueing","running"].includes(reposeProgress.phase)) reposeTracker?.cancel?.(); else closeRepose(); }}
|
||||
/>
|
||||
|
||||
<ExtendModal
|
||||
open={extendModalOpen}
|
||||
lastFrameUrl={extendPrepared?.lastFrameUrl || ""}
|
||||
fps={extendPrepared?.fps || 16}
|
||||
frameCount={extendPrepared?.frameCount || 0}
|
||||
lightningAvailable={extendPrepared?.lightningAvailable ?? true}
|
||||
defaults={extendPrepared?.defaults || null}
|
||||
recipeDefaults={extendPrepared?.recipeDefaults || null}
|
||||
mountPromptEditor={onMountPromptEditor}
|
||||
progress={extendProgress}
|
||||
onRun={confirmExtend}
|
||||
onDone={finishExtend}
|
||||
onCancel={() => { if (extendProgress && ["building","queueing","running"].includes(extendProgress.phase)) extendTracker?.cancel?.(); else closeExtend(); }}
|
||||
/>
|
||||
|
||||
<SmoothModal
|
||||
open={smoothModalOpen}
|
||||
fps={smoothPrepared?.fps || 16}
|
||||
frameCount={smoothPrepared?.frameCount || 0}
|
||||
progress={smoothProgress}
|
||||
onRun={confirmSmooth}
|
||||
onDone={finishSmooth}
|
||||
onCancel={() => { if (smoothProgress && ["building","queueing","running"].includes(smoothProgress.phase)) smoothTracker?.cancel?.(); else closeSmooth(); }}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
open={confirmDeleteOpen}
|
||||
title={deleteBatch?.length ? `Delete ${deleteBatch.length} images` : "Delete image"}
|
||||
@@ -2659,6 +3018,7 @@
|
||||
y={lineageMenu.y}
|
||||
canExpand={lineageMenu.item ? canExpand(lineageMenu.item) : false}
|
||||
canFocus={lineageMenu.item ? (lineageTree.childrenOf.get(lineageMenu.item.hash)?.length > 0 && lineageMenu.item.hash !== focusRootHash) : false}
|
||||
pinned={lineageMenu.item ? pinnedHashes.has(lineageMenu.item.hash) : false}
|
||||
canEdit={!!onEditSave}
|
||||
canDelete={!!onDelete}
|
||||
isLocal={isLocalClient}
|
||||
@@ -2746,6 +3106,28 @@
|
||||
}
|
||||
/* centered over the thumb so it never collides with the corner badges (dot,
|
||||
layers, fork pip / expander, opbadge) — the .selected ring already frames it */
|
||||
/* pin marker — top-left; shares the corner with the layers badge, which shifts right when both show */
|
||||
.pcr-lin-pinbadge {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
border-radius: 4px;
|
||||
background: rgba(20, 20, 20, 0.82);
|
||||
color: #ffd24a;
|
||||
box-shadow: 0 0 0 1px rgba(255, 200, 60, 0.6);
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.pcr-viewer-lineage-node.pinned .pcr-viewer-lineage-layers {
|
||||
left: 21px;
|
||||
}
|
||||
.pcr-lin-selcheck {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -3292,6 +3674,7 @@
|
||||
flex: 1;
|
||||
}
|
||||
.pcr-viewer-history-thumb {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -3386,6 +3769,7 @@
|
||||
.pcr-viewer-history-grid::-webkit-scrollbar-thumb:hover { background: #555; }
|
||||
|
||||
.pcr-viewer-grid-thumb {
|
||||
position: relative;
|
||||
width: 56.5px;
|
||||
height: 56.5px;
|
||||
border: 2px solid transparent;
|
||||
@@ -3399,6 +3783,27 @@
|
||||
.pcr-viewer-grid-thumb:hover:not(.current) { border-color: #666; transform: scale(1.05); z-index: 1; }
|
||||
.pcr-viewer-grid-thumb img { width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
|
||||
|
||||
/* ▶ overlay marking a poster-framed thumbnail as a playable video clip */
|
||||
.pcr-viewer-vidbadge {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding-left: 1px;
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
/* metadata content */
|
||||
.pcr-viewer-meta-content {
|
||||
flex: 1;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
x, y,
|
||||
canExpand = false,
|
||||
canFocus = false,
|
||||
pinned = false,
|
||||
canEdit = false,
|
||||
canDelete = false,
|
||||
isLocal = false,
|
||||
@@ -52,7 +53,8 @@
|
||||
{#if canFocus}
|
||||
<button class="pcr-ctx-item" onclick={() => act("focus-branch")}>Focus this branch</button>
|
||||
{/if}
|
||||
{#if (canExpand || canFocus) && (canEdit || canDelete || isLocal)}<div class="pcr-ctx-sep"></div>{/if}
|
||||
<button class="pcr-ctx-item" onclick={() => act("pin")}>{pinned ? "Unpin" : "Pin"}</button>
|
||||
{#if canEdit || canDelete || isLocal}<div class="pcr-ctx-sep"></div>{/if}
|
||||
{#if canEdit}
|
||||
<button class="pcr-ctx-item" onclick={() => act("edit")}>Edit</button>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<script>
|
||||
// "Smooth Video" modal — RIFE frame interpolation (2x/3x/4x fps) on the source
|
||||
// clip. Background graph (smooth-background.js); result records as a lineage
|
||||
// child. Multiplier is primary; model/scale/quality knobs live under Advanced.
|
||||
|
||||
let {
|
||||
open = false,
|
||||
fps = 16,
|
||||
frameCount = 0,
|
||||
progress = null,
|
||||
onRun,
|
||||
onDone, // () => land on the result + close (the "jump")
|
||||
onCancel,
|
||||
} = $props();
|
||||
|
||||
// Newer arches first; rife49 is the proven default and the one bundled. A ckpt
|
||||
// that isn't on disk auto-downloads on first render.
|
||||
const CKPTS = ["rife49.pth", "rife426.pth", "rife417.pth", "rife47.pth"];
|
||||
const SCALES = [0.25, 0.5, 1.0, 2.0, 4.0];
|
||||
|
||||
let mult = $state(2);
|
||||
let advancedOpen = $state(false);
|
||||
let ckptName = $state("rife49.pth");
|
||||
let scaleFactor = $state(1.0); // <1 for high-res (less VRAM); 1.0 default
|
||||
let ensemble = $state(true); // higher quality, slower
|
||||
let fastMode = $state(true); // faster, slight quality trade-off
|
||||
|
||||
let primed = false;
|
||||
$effect(() => {
|
||||
if (open && !primed) {
|
||||
mult = 2; advancedOpen = false; ckptName = "rife49.pth";
|
||||
scaleFactor = 1.0; ensemble = true; fastMode = true; primed = true;
|
||||
}
|
||||
if (!open) primed = false;
|
||||
});
|
||||
|
||||
let phase = $derived(progress?.phase || null);
|
||||
let running = $derived(phase === "building" || phase === "queueing" || phase === "running");
|
||||
let done = $derived(phase === "done");
|
||||
let pct = $derived(progress?.max ? Math.round((progress.value / progress.max) * 100) : 0);
|
||||
// RIFE outputs (frames-1)*mult + 1 frames; close enough to show the gist.
|
||||
let outFrames = $derived(frameCount ? (frameCount - 1) * mult + 1 : 0);
|
||||
|
||||
function run() {
|
||||
if (running) return;
|
||||
onRun?.({ multiplier: mult, ckptName, scaleFactor, ensemble, fastMode });
|
||||
}
|
||||
function close() { if (done) onDone?.(); else onCancel?.(); }
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div class="pcr-sm-overlay" onclick={(e) => { if (e.target === e.currentTarget && !running) close(); }}>
|
||||
<div class="pcr-sm-modal">
|
||||
<div class="pcr-sm-title">Smooth Video</div>
|
||||
<div class="pcr-sm-sub">
|
||||
Adds in-between frames with RIFE for smoother motion. {fps} → {fps * mult} fps{frameCount ? ` · ${frameCount} → ${outFrames} frames` : ""}. Saved as a lineage child.
|
||||
</div>
|
||||
|
||||
<div class="pcr-sm-label">Interpolation</div>
|
||||
<div class="pcr-sm-mults">
|
||||
{#each [2, 3, 4] as m}
|
||||
<button class="pcr-sm-mult" class:pcr-sm-active={mult === m} disabled={running} onclick={() => { mult = m; }}>{m}×</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="pcr-sm-adv-toggle" onclick={() => advancedOpen = !advancedOpen}>
|
||||
{advancedOpen ? "▾" : "▸"} Advanced
|
||||
</button>
|
||||
{#if advancedOpen}
|
||||
<div class="pcr-sm-adv">
|
||||
<label class="pcr-sm-field">
|
||||
<span>RIFE model</span>
|
||||
<select bind:value={ckptName} disabled={running}>
|
||||
{#each CKPTS as c}<option value={c}>{c.replace(".pth", "")}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="pcr-sm-field">
|
||||
<span>Scale <small>lower = less VRAM (high-res)</small></span>
|
||||
<select bind:value={scaleFactor} disabled={running}>
|
||||
{#each SCALES as s}<option value={s}>{s}×</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="pcr-sm-check">
|
||||
<input type="checkbox" bind:checked={ensemble} disabled={running} />
|
||||
<span>Ensemble <small>higher quality, slower</small></span>
|
||||
</label>
|
||||
<label class="pcr-sm-check">
|
||||
<input type="checkbox" bind:checked={fastMode} disabled={running} />
|
||||
<span>Fast mode <small>faster, slight quality trade-off</small></span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if progress}
|
||||
<div class="pcr-sm-progress">
|
||||
{#if phase === "error"}
|
||||
<div class="pcr-sm-err">⚠ {progress.message || "render failed"}</div>
|
||||
{:else if done}
|
||||
<div class="pcr-sm-done">✓ Smoothed. Adjust and Run Again, or Done to view it.</div>
|
||||
{#if progress.resultUrl}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video class="pcr-sm-result" src={progress.resultUrl} controls autoplay loop muted></video>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="pcr-sm-bar"><div class="pcr-sm-fill" style="width:{pct}%"></div></div>
|
||||
<div class="pcr-sm-phase">{phase}{progress.max ? ` · ${pct}%` : "…"}</div>
|
||||
{#if progress.previewUrl}
|
||||
<img class="pcr-sm-preview" src={progress.previewUrl} alt="preview" />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pcr-sm-actions">
|
||||
{#if running}
|
||||
<button class="pcr-sm-btn" onclick={() => onCancel?.()}>Cancel</button>
|
||||
{:else if done}
|
||||
<button class="pcr-sm-btn" onclick={() => onDone?.()}>Done</button>
|
||||
<button class="pcr-sm-btn pcr-sm-primary" onclick={run}>Run Again</button>
|
||||
{:else}
|
||||
<button class="pcr-sm-btn" onclick={() => onCancel?.()}>Close</button>
|
||||
<button class="pcr-sm-btn pcr-sm-primary" onclick={run}>Smooth</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.pcr-sm-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(3px);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 10011;
|
||||
}
|
||||
.pcr-sm-modal { background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 20px 24px; min-width: 360px; max-width: 460px; max-height: 88vh; overflow-y: auto; }
|
||||
.pcr-sm-title { font-size: 15px; font-weight: 600; color: #eee; margin-bottom: 4px; }
|
||||
.pcr-sm-sub { font-size: 11px; color: #888; margin-bottom: 14px; line-height: 1.4; }
|
||||
.pcr-sm-label { font-size: 11px; color: #aaa; margin-bottom: 6px; }
|
||||
.pcr-sm-mults { display: flex; gap: 8px; }
|
||||
.pcr-sm-mult {
|
||||
flex: 1; padding: 8px 0; border: 1px solid #555; border-radius: 4px; background: transparent;
|
||||
color: #ccc; font-size: 13px; cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.pcr-sm-mult:hover { border-color: #888; color: #fff; }
|
||||
.pcr-sm-mult.pcr-sm-active { border-color: #4fc3f7; color: #4fc3f7; background: rgba(79,195,247,0.1); }
|
||||
.pcr-sm-adv-toggle { margin-top: 12px; background: none; border: none; color: #8bd5f5; font-size: 11px; cursor: pointer; padding: 2px 0; }
|
||||
.pcr-sm-adv { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.pcr-sm-field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: #aaa; }
|
||||
.pcr-sm-field small, .pcr-sm-check small { color: #777; }
|
||||
.pcr-sm-field select {
|
||||
background: #161616; border: 1px solid #444; border-radius: 4px; color: #ddd;
|
||||
font-size: 12px; padding: 5px 6px; font-family: inherit;
|
||||
}
|
||||
.pcr-sm-check { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #aaa; cursor: pointer; }
|
||||
.pcr-sm-progress { margin-top: 14px; }
|
||||
.pcr-sm-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
|
||||
.pcr-sm-fill { height: 100%; background: #4fc3f7; border-radius: 3px; transition: width 0.3s; }
|
||||
.pcr-sm-phase { font-size: 11px; color: #aaa; margin-top: 6px; text-transform: capitalize; }
|
||||
.pcr-sm-preview, .pcr-sm-result { width: 100%; margin-top: 8px; border-radius: 4px; max-height: 240px; }
|
||||
.pcr-sm-err { font-size: 12px; color: #f44336; }
|
||||
.pcr-sm-done { font-size: 12px; color: #4caf50; margin-bottom: 6px; }
|
||||
.pcr-sm-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
||||
.pcr-sm-btn { padding: 6px 16px; font-size: 12px; border: 1px solid #555; border-radius: 4px; background: transparent; color: #ccc; cursor: pointer; }
|
||||
.pcr-sm-btn:hover { background: rgba(255,255,255,0.08); }
|
||||
.pcr-sm-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.pcr-sm-primary { background: #4fc3f7; color: #111; border-color: #4fc3f7; }
|
||||
.pcr-sm-primary:hover { background: #39b0e4; }
|
||||
.pcr-sm-primary:disabled { background: #2a7a9e; }
|
||||
</style>
|
||||
@@ -383,16 +383,41 @@
|
||||
});
|
||||
|
||||
function thumbUrl(hash) { return apiURL(`/promptchain/thumb/${hash}`); }
|
||||
// A thumbnail <img> can latch onto a 404 from a request that raced the record
|
||||
// (the thumb is generated server-side as part of recording); a broken <img>
|
||||
// never retries itself. Cache-bust and re-fetch ONCE on the error event — the
|
||||
// thumb is on disk by now. Event-driven, no polling.
|
||||
// A thumbnail <img> can latch broken if its request raced the record (the
|
||||
// thumb is generated server-side as part of recording). A broken <img> never
|
||||
// retries itself, so cache-bust and re-fetch on the natural error event —
|
||||
// bounded, event-driven, no polling.
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
if (img.dataset.pcrRetried) return;
|
||||
img.dataset.pcrRetried = "1";
|
||||
img.src = img.src + (img.src.includes("?") ? "&" : "?") + "r=1";
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
|
||||
// A thumb that exhausted its retries while the record was still committing
|
||||
// stays broken forever. The authoritative "record committed" signal is the
|
||||
// generation-recorded event (fired AFTER recordGeneration's awaited POST, so
|
||||
// the thumb is provably on disk by now) — re-fetch any latched-broken <img>
|
||||
// when it lands. Event-driven recovery, no timers.
|
||||
function healBrokenThumbs() {
|
||||
if (!galleryEl) return;
|
||||
for (const img of galleryEl.querySelectorAll("img")) {
|
||||
if (img.complete && img.naturalWidth === 0) {
|
||||
img.dataset.pcrRetry = "0";
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", "heal");
|
||||
img.src = u.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
const onRecorded = () => healBrokenThumbs();
|
||||
window.addEventListener("promptchain:generation-recorded", onRecorded);
|
||||
return () => window.removeEventListener("promptchain:generation-recorded", onRecorded);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
|
||||
@@ -128,6 +128,23 @@
|
||||
editors[nodeType][widgetName] = val;
|
||||
}
|
||||
|
||||
// Ideogram's noise schedule is computed by Ideogram4Scheduler, which carries its
|
||||
// OWN width/height that must match the latent or the schedule and the image size
|
||||
// disagree. The resolution row writes only the latent node, so slave the
|
||||
// scheduler's width/height to it here (it is intentionally not shown as a second
|
||||
// resolution row). No-op on any graph without an Ideogram4Scheduler section.
|
||||
function mirrorIdeogramScheduler(latentNode) {
|
||||
const sched = detected.find((d) => d.type === "Ideogram4Scheduler");
|
||||
if (!sched) return;
|
||||
for (const dim of ["width", "height"]) {
|
||||
const v = readWidgetValue(latentNode, dim);
|
||||
if (v !== undefined) {
|
||||
writeWidgetValue(sched.node, dim, v);
|
||||
setEditorValue("Ideogram4Scheduler", dim, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setRangeValue(nodeType, widgetName, lo, hi) {
|
||||
if (!rangeValues[nodeType]) rangeValues[nodeType] = {};
|
||||
rangeValues[nodeType][`${widgetName}_range`] = [lo, hi];
|
||||
@@ -151,6 +168,22 @@
|
||||
presetMenuOpen[nodeType] = !presetMenuOpen[nodeType];
|
||||
}
|
||||
|
||||
// Close an open preset menu when the pointer goes down anywhere outside its
|
||||
// wrap. Capture phase so it runs before the toggle button's own handler; the
|
||||
// wrap holds both the button and the menu, so toggling/selecting still works.
|
||||
// (The modal's own outside-click ignores clicks inside the menu, so this is
|
||||
// the only thing that dismisses it on an unrelated click.) Event-driven.
|
||||
$effect(() => {
|
||||
function onDocDown(e) {
|
||||
if (e.target?.closest?.(".pcr-resolution-preset-wrap")) return;
|
||||
for (const k of Object.keys(presetMenuOpen)) {
|
||||
if (presetMenuOpen[k]) presetMenuOpen[k] = false;
|
||||
}
|
||||
}
|
||||
document.addEventListener("pointerdown", onDocDown, true);
|
||||
return () => document.removeEventListener("pointerdown", onDocDown, true);
|
||||
});
|
||||
|
||||
// Collapsible section state. The incoming prop is a plain object (not
|
||||
// $state), so mutating it directly doesn't re-render. Drive a local $state
|
||||
// and mirror back to the prop so expand state still persists across reopens.
|
||||
@@ -270,20 +303,22 @@
|
||||
{#if !rangeMode}
|
||||
{@const hasResolution = Object.values(config.widgets).some(d => d.type === "resolution")}
|
||||
{#if hasResolution}
|
||||
{@const w = readWidgetValue(node, "width")}
|
||||
{@const h = readWidgetValue(node, "height")}
|
||||
{@const w = editors[type]?.width}
|
||||
{@const h = editors[type]?.height}
|
||||
{#if w !== undefined && h !== undefined}
|
||||
{@const savedW = savedWidgets["width"]}
|
||||
{@const savedH = savedWidgets["height"]}
|
||||
{@const presets = savedWidgets["_resolutions"] || []}
|
||||
{@const presets = editors[type]?.["_resolutions"] || []}
|
||||
{@const isPreset = presets.some(p => p.width === w && p.height === h)}
|
||||
<div class="pcr-model-panel-row pcr-resolution-row">
|
||||
<span class="pcr-model-panel-label">resolution</span>
|
||||
<div class="pcr-resolution-controls">
|
||||
<div class="pcr-resolution-preset-wrap">
|
||||
<button class="pcr-resolution-preset-btn"
|
||||
class:pcr-resolution-preset-active={presets.some(p => p.width === w && p.height === h)}
|
||||
class:pcr-resolution-preset-active={isPreset}
|
||||
class:pcr-resolution-preset-dimmed={!isPreset}
|
||||
onclick={(e) => { e.stopPropagation(); togglePresetMenu(type); }}>
|
||||
{presets.some(p => p.width === w && p.height === h) ? `${w}×${h}` : "Presets"}
|
||||
{isPreset ? `${w}×${h}` : "Presets"}
|
||||
</button>
|
||||
{#if presetMenuOpen[type]}
|
||||
<div class="pcr-resolution-preset-menu" style="display:block">
|
||||
@@ -303,6 +338,7 @@
|
||||
writeWidgetValue(node, "height", p.height);
|
||||
setEditorValue(type, "width", p.width);
|
||||
setEditorValue(type, "height", p.height);
|
||||
mirrorIdeogramScheduler(node);
|
||||
presetMenuOpen[type] = false;
|
||||
}}>
|
||||
<span class="pcr-resolution-preset-item-label">{p.width}×{p.height}</span>
|
||||
@@ -318,35 +354,38 @@
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="pcr-resolution-preset-add"
|
||||
class:pcr-resolution-preset-add-disabled={presets.some(p => p.width === w && p.height === h)}
|
||||
class:pcr-resolution-preset-add-disabled={isPreset}
|
||||
onclick={() => {
|
||||
if (!presets.some(p => p.width === w && p.height === h)) {
|
||||
presets.push({ width: w, height: h });
|
||||
editors[type]["_resolutions"] = [...presets];
|
||||
if (!isPreset) {
|
||||
editors[type]["_resolutions"] = [...presets, { width: w, height: h }];
|
||||
}
|
||||
presetMenuOpen[type] = false;
|
||||
}}>
|
||||
{presets.some(p => p.width === w && p.height === h) ? `${w}×${h} already saved` : "+ Add Current"}
|
||||
{isPreset ? `${w}×${h} already saved` : "+ Add Current"}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<input type="number" class="pcr-resolution-input"
|
||||
class:pcr-resolution-input-dimmed={presets.some(p => p.width === w && p.height === h)}
|
||||
class:pcr-resolution-input-dimmed={isPreset}
|
||||
class:pcr-resolution-input-active={!isPreset}
|
||||
value={w} step={8}
|
||||
onchange={(e) => {
|
||||
const v = parseInt(e.target.value) || 512;
|
||||
setEditorValue(type, "width", v);
|
||||
writeWidgetValue(node, "width", v);
|
||||
mirrorIdeogramScheduler(node);
|
||||
}} />
|
||||
<span class="pcr-resolution-sep">×</span>
|
||||
<input type="number" class="pcr-resolution-input"
|
||||
class:pcr-resolution-input-dimmed={presets.some(p => p.width === w && p.height === h)}
|
||||
class:pcr-resolution-input-dimmed={isPreset}
|
||||
class:pcr-resolution-input-active={!isPreset}
|
||||
value={h} step={8}
|
||||
onchange={(e) => {
|
||||
const v = parseInt(e.target.value) || 512;
|
||||
setEditorValue(type, "height", v);
|
||||
writeWidgetValue(node, "height", v);
|
||||
mirrorIdeogramScheduler(node);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -726,9 +765,18 @@
|
||||
color: #5dcaff;
|
||||
min-width: 90px;
|
||||
}
|
||||
/* custom (non-preset) resolution: dim the dropdown, light up the W×H boxes */
|
||||
.pcr-resolution-preset-dimmed {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.pcr-resolution-input-dimmed {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.pcr-resolution-input-active {
|
||||
border-color: #5dcaff;
|
||||
color: #fff;
|
||||
background: #222b33;
|
||||
}
|
||||
.pcr-resolution-preset-menu {
|
||||
position: fixed;
|
||||
background: rgba(30, 30, 30, 0.98);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
let panelEl;
|
||||
let dividerEl;
|
||||
let imgEl;
|
||||
let videoEl;
|
||||
let imgContainerEl;
|
||||
let isVisible = $state(!!node.properties?.pcrImagePreview);
|
||||
let panelWidth = $state(node.properties?.pcrImagePanelWidth || DEFAULT_WIDTH);
|
||||
@@ -106,6 +107,13 @@
|
||||
} catch { return { filename: "", subfolder: "" }; }
|
||||
}
|
||||
|
||||
// Video outputs (Wan/AnimateDiff/…) arrive through the same executed event as
|
||||
// images but name an .mp4/.webm file — render those in a <video>, not <img>.
|
||||
let isVideoUrl = $derived.by(() => {
|
||||
if (!currentImageUrl) return false;
|
||||
return /\.(mp4|webm|m4v|mov)(\?|$)/i.test(parseImageUrl(currentImageUrl).filename);
|
||||
});
|
||||
|
||||
let filenameDisplay = $derived.by(() => {
|
||||
if (shared.isGenerating) return "Generating\u2026";
|
||||
if (!currentImageUrl) return "";
|
||||
@@ -171,6 +179,13 @@
|
||||
if (!isPreviewMode) resetZoom();
|
||||
}
|
||||
|
||||
function handleVideoLoaded() {
|
||||
imageLoaded = true;
|
||||
imageError = false;
|
||||
naturalWidth = videoEl?.videoWidth || 0;
|
||||
naturalHeight = videoEl?.videoHeight || 0;
|
||||
}
|
||||
|
||||
function handleImageError() {
|
||||
imageLoaded = false;
|
||||
imageError = true;
|
||||
@@ -475,7 +490,21 @@
|
||||
onpointerup={handlePanEnd}
|
||||
ondblclick={(e) => { e.stopPropagation(); resetZoom(true); }}
|
||||
>
|
||||
{#if currentImageUrl}
|
||||
{#if currentImageUrl && isVideoUrl}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video
|
||||
bind:this={videoEl}
|
||||
src={currentImageUrl}
|
||||
style:display={imageLoaded ? "block" : "none"}
|
||||
onloadeddata={handleVideoLoaded}
|
||||
onerror={handleImageError}
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
playsinline
|
||||
controls
|
||||
></video>
|
||||
{:else if currentImageUrl}
|
||||
<img
|
||||
bind:this={imgEl}
|
||||
src={currentImageUrl}
|
||||
@@ -620,7 +649,8 @@
|
||||
overflow: hidden;
|
||||
background: var(--pcr-fs-editor-surface, transparent);
|
||||
}
|
||||
.pcr-image-container img {
|
||||
.pcr-image-container img,
|
||||
.pcr-image-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
onToggleImage = () => {},
|
||||
onToggleAssistant = () => {},
|
||||
onTogglePose = () => {},
|
||||
onToggleRegion = () => {},
|
||||
onOpenFullscreen = () => {},
|
||||
onResetIterate = null,
|
||||
docDropdownEl = null,
|
||||
@@ -131,6 +132,7 @@
|
||||
else if (action === "output") onToggleOutput();
|
||||
else if (action === "assistant") { if (aiAssistantEnabled) onToggleAssistant(); }
|
||||
else if (action === "pose") { if (shared.hasPoseStudio) onTogglePose(); }
|
||||
else if (action === "region") { if (shared.hasRegionBox) onToggleRegion(); }
|
||||
else if (action === "collapse") onToggleCollapse();
|
||||
else if (action === "maximize") onOpenFullscreen();
|
||||
}
|
||||
@@ -219,6 +221,18 @@
|
||||
{@html '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>'}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="pcr-menubar-btn"
|
||||
class:pcr-menubar-btn-active={shared.regionPanelVisible}
|
||||
class:pcr-menubar-btn-disabled={!shared.hasRegionBox}
|
||||
style:color={shared.regionPanelVisible ? "#42b9c4" : ""}
|
||||
title={shared.hasRegionBox ? "Toggle Region Box panel" : "Add a Region Box node to use this"}
|
||||
data-action="region"
|
||||
use:actionClick={"region"}
|
||||
>
|
||||
{@html '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5v4h2V5h4V3H5c-1.1 0-2 .9-2 2zm2 10H3v4c0 1.1.9 2 2 2h4v-2H5v-4zm14 4h-4v2h4c1.1 0 2-.9 2-2v-4h-2v4zm0-16h-4v2h4v4h2V5c0-1.1-.9-2-2-2zM7 7v10h10V7H7zm8 8H9V9h6v6z"/></svg>'}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="pcr-menubar-btn"
|
||||
class:pcr-menubar-btn-active={shared.imagePanelVisible}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import OutputPanel from "./OutputPanel.svelte";
|
||||
import ImagePanel from "./ImagePanel.svelte";
|
||||
import PosePanel from "./PosePanel.svelte";
|
||||
import RegionPanel from "./RegionPanel.svelte";
|
||||
import Footer from "./Footer.svelte";
|
||||
import AIAssistant from "./AIAssistant.svelte";
|
||||
|
||||
@@ -24,6 +25,7 @@
|
||||
onToggleImage = () => {},
|
||||
onToggleAssistant = () => {},
|
||||
onTogglePose = () => {},
|
||||
onToggleRegion = () => {},
|
||||
onOpenFullscreen = () => {},
|
||||
// doc dropdown DOM element (imperative)
|
||||
docDropdownEl = null,
|
||||
@@ -37,10 +39,13 @@
|
||||
onOutputPanelRegister = null,
|
||||
onImagePanelRegister = null,
|
||||
onPosePanelRegister = null,
|
||||
onRegionPanelRegister = null,
|
||||
onAIAssistantRegister = null,
|
||||
onFooterRegister = null,
|
||||
// returns the active 3D Poser node to dock into the pose panel
|
||||
getActivePoser = () => null,
|
||||
// returns the active Region Box node to dock into the region panel
|
||||
getActiveRegionBox = () => null,
|
||||
// OutputPanel API callbacks
|
||||
apiURL = (p) => p,
|
||||
fetchApi = (p, o) => fetch(p, o),
|
||||
@@ -83,6 +88,7 @@
|
||||
onToggleImage={() => onToggleImage()}
|
||||
onToggleAssistant={() => onToggleAssistant()}
|
||||
onTogglePose={() => onTogglePose()}
|
||||
onToggleRegion={() => onToggleRegion()}
|
||||
{onOpenFullscreen}
|
||||
{docDropdownEl}
|
||||
/>
|
||||
@@ -98,6 +104,14 @@
|
||||
onToggle={(visible) => { shared.posePanelVisible = visible; }}
|
||||
onRegister={onPosePanelRegister}
|
||||
/>
|
||||
<!-- Region Box panel — also leftmost. Docks the live Region Box canvas. -->
|
||||
<RegionPanel
|
||||
{node}
|
||||
{shared}
|
||||
{getActiveRegionBox}
|
||||
onToggle={(visible) => { shared.regionPanelVisible = visible; }}
|
||||
onRegister={onRegionPanelRegister}
|
||||
/>
|
||||
<!-- AIAssistant is always rendered so its DOM can be relocated into
|
||||
the fullscreen pane-row when entering fullscreen; visibility
|
||||
toggles via display:none. Same lifecycle pattern as ImagePanel. -->
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<script>
|
||||
// Region Box panel — left-side surface that hosts the live PromptChain_RegionBox
|
||||
// canvas. Opening it reparents ("docks") the active Region Box node's canvas DOM
|
||||
// into this panel so regions are drawn from one roomy spot instead of the cramped
|
||||
// node body — and, because it's part of the PromptChain node's own widget tree,
|
||||
// it stays bound to that node.
|
||||
//
|
||||
// The canvas is a single relocatable element; we move it in/out rather than
|
||||
// duplicating it. getActiveRegionBox() comes from main.js, which shares the
|
||||
// region-box registry with region-box.js (a direct import here would bundle a
|
||||
// second, non-shared copy). Twin of PosePanel.svelte.
|
||||
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { useApi } from "../../lib/api-context.js";
|
||||
|
||||
const DEFAULT_WIDTH = 440;
|
||||
const MIN_WIDTH = 240;
|
||||
|
||||
let {
|
||||
node,
|
||||
shared,
|
||||
onToggle = () => {},
|
||||
onRegister = null,
|
||||
getActiveRegionBox = () => null,
|
||||
} = $props();
|
||||
|
||||
const { getCanvasScale } = useApi();
|
||||
|
||||
let panelEl;
|
||||
let dividerEl;
|
||||
let bodyEl;
|
||||
let isVisible = $state(!!node.properties?.pcrRegionPanel);
|
||||
let panelWidth = $state(node.properties?.pcrRegionPanelWidth || DEFAULT_WIDTH);
|
||||
// The Region Box node whose canvas is currently docked here (so hide()/refresh
|
||||
// can return it home). Plain let — its identity drives imperative dock calls,
|
||||
// not template reactivity; `hasDock` mirrors it for the placeholder.
|
||||
let dockedNode = null;
|
||||
let hasDock = $state(false);
|
||||
|
||||
let toggleListener = null;
|
||||
function emitToggle(visible) {
|
||||
onToggle(visible);
|
||||
toggleListener?.(visible);
|
||||
}
|
||||
export function setToggleListener(cb) { toggleListener = cb; }
|
||||
|
||||
function dock(rb) {
|
||||
if (!rb || !bodyEl || !rb._pcrRegion?.enterDock) return;
|
||||
rb._pcrRegion.enterDock(bodyEl);
|
||||
dockedNode = rb;
|
||||
hasDock = true;
|
||||
}
|
||||
|
||||
function undock() {
|
||||
if (dockedNode && dockedNode._pcrRegion?.exitDock) {
|
||||
try { dockedNode._pcrRegion.exitDock(); } catch (e) { console.error("[PromptChain] region undock error", e); }
|
||||
}
|
||||
dockedNode = null;
|
||||
hasDock = false;
|
||||
}
|
||||
|
||||
// Re-evaluate which Region Box is hosted. Called by main.js when the registry
|
||||
// changes. Keep the current host while it's alive; only re-dock when it's gone.
|
||||
export function refreshDock() {
|
||||
if (!isVisible) return;
|
||||
if (dockedNode && dockedNode._pcrAlive) return;
|
||||
if (dockedNode) undock(); // host died — release before re-docking
|
||||
const active = getActiveRegionBox();
|
||||
if (active) dock(active);
|
||||
}
|
||||
|
||||
export function show() {
|
||||
isVisible = true;
|
||||
if (node.properties) node.properties.pcrRegionPanel = true;
|
||||
emitToggle(true);
|
||||
requestAnimationFrame(() => { if (isVisible) dock(getActiveRegionBox()); });
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
undock();
|
||||
isVisible = false;
|
||||
if (node.properties) node.properties.pcrRegionPanel = false;
|
||||
emitToggle(false);
|
||||
}
|
||||
|
||||
export function toggle() { isVisible ? hide() : show(); }
|
||||
export function getIsVisible() { return isVisible; }
|
||||
|
||||
function getMaxPanelWidth() {
|
||||
const row = panelEl?.parentElement;
|
||||
const rowWidth = row?.offsetWidth || 0;
|
||||
if (!rowWidth) return Number.POSITIVE_INFINITY;
|
||||
return Math.max(MIN_WIDTH, rowWidth - 120);
|
||||
}
|
||||
|
||||
function clampPanelWidth(value) {
|
||||
return Math.min(Math.max(MIN_WIDTH, value), getMaxPanelWidth());
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
onRegister?.({ toggle, show, hide, getIsVisible, refreshDock, setToggleListener, cleanup });
|
||||
if (node.properties?.pcrRegionPanel) requestAnimationFrame(() => show());
|
||||
requestAnimationFrame(() => { panelWidth = clampPanelWidth(panelWidth); });
|
||||
});
|
||||
|
||||
// divider resize — panel is leftmost, so dragging the divider right widens it.
|
||||
let dividerAc;
|
||||
onMount(() => {
|
||||
dividerAc = new AbortController();
|
||||
let isDragging = false;
|
||||
let startX = 0;
|
||||
let startWidth = 0;
|
||||
const getScale = () => {
|
||||
const inFs = !!document.querySelector(".pcr-fs-overlay");
|
||||
return inFs ? 1 : getCanvasScale();
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", (e) => {
|
||||
if (!dividerEl || (e.target !== dividerEl && !dividerEl.contains(e.target))) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startWidth = panelEl?.offsetWidth || panelWidth;
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const canvas = document.querySelector("canvas.lgraphcanvas");
|
||||
if (canvas && typeof e.pointerId === "number") {
|
||||
try { if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId); } catch {}
|
||||
}
|
||||
}, { capture: true, signal: dividerAc.signal });
|
||||
|
||||
document.addEventListener("pointermove", (e) => {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const delta = (e.clientX - startX) / getScale();
|
||||
panelWidth = clampPanelWidth(startWidth + delta);
|
||||
}, { capture: true, signal: dividerAc.signal });
|
||||
|
||||
document.addEventListener("pointerup", () => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
if (node.properties) node.properties.pcrRegionPanelWidth = panelWidth;
|
||||
}, { capture: true, signal: dividerAc.signal });
|
||||
});
|
||||
|
||||
const SHOW_CLOSE_BTN = false; // ✕ hidden; toggle via the menubar button
|
||||
let closeState = $state("hidden"); // hidden | dim | full
|
||||
function onPanelMove(e) {
|
||||
if (!panelEl) return;
|
||||
const r = panelEl.getBoundingClientRect();
|
||||
const inTop = e.clientY - r.top < Math.max(32, r.height * 0.1);
|
||||
const inRight = r.right - e.clientX < Math.max(32, r.width * 0.1);
|
||||
closeState = inTop && inRight ? "full" : "dim";
|
||||
}
|
||||
function onPanelLeave() {
|
||||
closeState = "hidden";
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
dividerAc?.abort();
|
||||
undock();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
dividerAc?.abort();
|
||||
undock();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={panelEl}
|
||||
class="pcr-region-panel"
|
||||
style:width="{panelWidth}px"
|
||||
style:display={isVisible ? "flex" : "none"}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
ondblclick={(e) => e.stopPropagation()}
|
||||
onpointerleave={onPanelLeave}
|
||||
onpointermovecapture={onPanelMove}
|
||||
>
|
||||
{#if SHOW_CLOSE_BTN}
|
||||
<button class="pcr-region-panel-close-btn" class:pcr-close-dim={closeState === "dim"} class:pcr-close-show={closeState === "full"} title="Close Region Box panel" onclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hide();
|
||||
}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- the Region Box canvas is reparented into here on dock -->
|
||||
<div bind:this={bodyEl} class="pcr-region-panel-body">
|
||||
{#if !hasDock}
|
||||
<div class="pcr-region-panel-placeholder">No Region Box node in the graph</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div bind:this={dividerEl} class="pcr-region-divider" style:display={isVisible ? "flex" : "none"}
|
||||
ondblclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const rb = dockedNode || getActiveRegionBox();
|
||||
const w = rb?.widgets?.find((x) => x.name === "width")?.value;
|
||||
const h = rb?.widgets?.find((x) => x.name === "height")?.value;
|
||||
if (!w || !h) return;
|
||||
const bodyH = bodyEl?.offsetHeight || panelEl?.offsetHeight || 300;
|
||||
panelWidth = clampPanelWidth(Math.round(bodyH * (w / h)));
|
||||
if (node.properties) node.properties.pcrRegionPanelWidth = panelWidth;
|
||||
}}
|
||||
></div>
|
||||
|
||||
<style>
|
||||
.pcr-region-panel {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
min-width: 240px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
:global(.lg-node-widgets) .pcr-region-panel {
|
||||
background: rgb(0 0 0 / 46%);
|
||||
}
|
||||
.pcr-region-panel-close-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 0 3px rgba(0, 0, 0, 0.7));
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
.pcr-region-panel-close-btn.pcr-close-dim {
|
||||
opacity: 0.3;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-region-panel-close-btn.pcr-close-show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pcr-region-panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--pcr-fs-editor-surface, transparent);
|
||||
}
|
||||
.pcr-region-panel-placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0 12px;
|
||||
user-select: none;
|
||||
}
|
||||
.pcr-region-divider {
|
||||
width: 4px;
|
||||
background: #242424;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.pcr-region-divider::before {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 1px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.pcr-region-divider:hover::before {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,9 @@
|
||||
catalogEntry,
|
||||
onClose,
|
||||
onModelReady,
|
||||
// Pre-selects a precision (e.g. the "FP16 ↓" tag the user clicked on an
|
||||
// installed model row). Falls back to the first declared precision.
|
||||
initialPrecision = null,
|
||||
} = $props();
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────
|
||||
@@ -54,7 +57,9 @@
|
||||
const rawFiles = catalogEntry.files || [];
|
||||
const precisions = extractPrecisions(rawFiles);
|
||||
|
||||
let selectedPrecision = $state(precisions[0] || null);
|
||||
let selectedPrecision = $state(
|
||||
(initialPrecision && precisions.includes(initialPrecision)) ? initialPrecision : (precisions[0] || null)
|
||||
);
|
||||
let resolvedFiles = $derived(
|
||||
selectedPrecision ? resolveFilesForPrecision(rawFiles, selectedPrecision) : rawFiles
|
||||
);
|
||||
|
||||
@@ -71,8 +71,17 @@
|
||||
let searchEl;
|
||||
let panelStyle = $state("");
|
||||
|
||||
// Catalog precision installation status: { entryHash: { precision: bool } }
|
||||
let catalogPrecisionStatus = $state({});
|
||||
// Per-precision install state for every catalog entry, authoritative from the
|
||||
// backend ("installed" | "partial" | "missing"). Derived straight from the
|
||||
// catalog payload — no client-side file probing, so it can't drift from the
|
||||
// real _all_files_installed check the way the old JS heuristic did (it only
|
||||
// probed the diffusion file and flagged a precision installed while a
|
||||
// companion encoder/VAE was still missing).
|
||||
let catalogPrecisionStatus = $derived.by(() => {
|
||||
const out = {};
|
||||
for (const c of catalog) out[c.hash] = c.precision_status || {};
|
||||
return out;
|
||||
});
|
||||
|
||||
// ── load data ──
|
||||
async function loadData() {
|
||||
@@ -98,9 +107,6 @@
|
||||
modelSettings = settingsRes.settings || {};
|
||||
}
|
||||
|
||||
// Check catalog precision installation status
|
||||
await checkCatalogPrecisions();
|
||||
|
||||
loading = false;
|
||||
requestAnimationFrame(() => searchEl?.focus());
|
||||
focusDownloadedModel();
|
||||
@@ -137,40 +143,6 @@
|
||||
listEl?.querySelector(".pcr-picker-item-highlight")?.scrollIntoView({ block: "center" });
|
||||
}
|
||||
|
||||
async function checkCatalogPrecisions() {
|
||||
if (!catalog.length) return;
|
||||
const filesToCheck = [];
|
||||
const fileMap = [];
|
||||
const status = {};
|
||||
for (const entry of catalog) {
|
||||
status[entry.hash] = {};
|
||||
for (const f of entry.files || []) {
|
||||
if (!f.variants) continue;
|
||||
for (const v of f.variants) {
|
||||
if (f.folder === "diffusion_models" || f.folder === "unet") {
|
||||
filesToCheck.push({ filename: v.filename, folder: f.folder });
|
||||
fileMap.push({ entryHash: entry.hash, precision: v.precision });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!filesToCheck.length) { catalogPrecisionStatus = status; return; }
|
||||
try {
|
||||
const checkRes = await fetch("/promptchain/models/check-files", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ files: filesToCheck }),
|
||||
});
|
||||
const checkData = await checkRes.json();
|
||||
for (let i = 0; i < (checkData.results || []).length; i++) {
|
||||
if (checkData.results[i].exists) {
|
||||
status[fileMap[i].entryHash][fileMap[i].precision] = true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
catalogPrecisionStatus = status;
|
||||
}
|
||||
|
||||
// ── grouped models ──
|
||||
let groupedModels = $derived.by(() => {
|
||||
// Sort models alphabetically by display name first
|
||||
@@ -247,7 +219,10 @@
|
||||
const terms = q ? q.split(/\s+/) : [];
|
||||
return catalog.filter(c => {
|
||||
const status = catalogPrecisionStatus[c.hash] || {};
|
||||
if (!Object.values(status).some(v => v)) return false;
|
||||
// In the precision-tag strip when at least one precision has files on disk
|
||||
// (installed, or a resumable partial). All-missing entries drop to the
|
||||
// grouped "available for download" list below.
|
||||
if (!Object.values(status).some(v => v === "installed" || v === "partial")) return false;
|
||||
if (!terms.length) return true;
|
||||
const text = [c.display_name, c.model_name, c.architecture, c.family, c.version].join(" ").toLowerCase();
|
||||
return terms.every(t => text.includes(t));
|
||||
@@ -257,7 +232,7 @@
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
let items = catalog.filter(c => {
|
||||
const status = catalogPrecisionStatus[c.hash] || {};
|
||||
return !Object.values(status).some(v => v);
|
||||
return !Object.values(status).some(v => v === "installed" || v === "partial");
|
||||
});
|
||||
if (q) {
|
||||
const terms = q.split(/\s+/);
|
||||
@@ -282,10 +257,13 @@
|
||||
name: entry.model_name || entry.display_name || "",
|
||||
architecture: entry.architecture || "",
|
||||
family: entry.family || "",
|
||||
partial: false,
|
||||
versions: [],
|
||||
});
|
||||
}
|
||||
groups.get(key).versions.push(entry);
|
||||
const g = groups.get(key);
|
||||
g.versions.push(entry);
|
||||
if (entry.partial) g.partial = true;
|
||||
}
|
||||
for (const g of groups.values()) {
|
||||
g.versions.sort((a, b) => (b.version || "").localeCompare(a.version || "", undefined, { numeric: true }));
|
||||
@@ -429,16 +407,21 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCatalogPrecisionClick(entry, prec, installed) {
|
||||
if (!installed) {
|
||||
onShowCatalogDownload(entry);
|
||||
async function handleCatalogPrecisionClick(entry, prec, state) {
|
||||
// Anything not fully present opens the download modal, which re-checks each
|
||||
// file (with size) and offers "Download N Missing" — so a partial download
|
||||
// resumes the missing companion instead of restarting from scratch.
|
||||
if (state !== "installed") {
|
||||
onShowCatalogDownload(entry, prec);
|
||||
return;
|
||||
}
|
||||
// Installed: find the local model for this precision and show templates
|
||||
// Installed: jump to this precision's templates. If the model can't be
|
||||
// resolved (e.g. files on disk but not yet scanned), fall back to the
|
||||
// download modal rather than silently closing — that onClose() dead-end is
|
||||
// exactly what left partial entries looking installed yet unclickable.
|
||||
const resolved = resolveFilesForPrecision(entry.files, prec);
|
||||
const diffFile = resolved.find(f => f.folder === "diffusion_models" || f.folder === "unet");
|
||||
if (!diffFile) return;
|
||||
const localModel = models.find(m => m.filename === diffFile.filename);
|
||||
const localModel = diffFile && models.find(m => m.filename === diffFile.filename);
|
||||
if (localModel && templatesForModel(localModel).length > 0) {
|
||||
activeSubmenu = {
|
||||
type: "template",
|
||||
@@ -446,11 +429,83 @@
|
||||
displayName: modelSettings[localModel.hash]?.display_name || localModel.filename,
|
||||
};
|
||||
activeNestedSubmenu = null;
|
||||
} else {
|
||||
onShowCatalogDownload(entry, prec);
|
||||
}
|
||||
}
|
||||
|
||||
// ── installed-model precision upgrades ──
|
||||
// An installed model whose config declares several precisions for its
|
||||
// primary file (e.g. Wan fp8_scaled + fp16) but only has some on disk can be
|
||||
// "moved up" to a heavier precision. We surface the declared precisions as
|
||||
// inline tags on the installed row: present ones jump to templates, missing
|
||||
// ones open the download modal. Detection is client-side — modelSettings
|
||||
// already ships the config's files, and the models list tells us which
|
||||
// primary files are on disk.
|
||||
|
||||
// The config's primary diffusion/unet file entry — its variants are the
|
||||
// upgrade options. Mirrors the backend is_primary_model_file: an explicit
|
||||
// primary:true wins, else the first diffusion/unet entry.
|
||||
function primaryFileEntry(settings) {
|
||||
const files = settings?.files || [];
|
||||
const isPrimaryFolder = f => f.folder === "diffusion_models" || f.folder === "unet" || f.folder === "checkpoints";
|
||||
return files.find(f => f.primary && isPrimaryFolder(f))
|
||||
|| files.find(isPrimaryFolder)
|
||||
|| null;
|
||||
}
|
||||
|
||||
// Declared primary precisions for an installed group, each tagged with
|
||||
// whether its file is on disk. Empty unless the config has 2+ variants
|
||||
// (nothing to upgrade otherwise).
|
||||
function groupPrecisions(group) {
|
||||
const settings = modelSettings[group.versions[0]?.hash];
|
||||
const variants = primaryFileEntry(settings)?.variants || [];
|
||||
if (variants.length < 2) return [];
|
||||
return variants.map(v => {
|
||||
const fnameLower = (v.filename || "").toLowerCase();
|
||||
const localModel = models.find(m => (m.filename || "").toLowerCase() === fnameLower);
|
||||
return { precision: v.precision, filename: v.filename, installed: !!localModel, model: localModel || null };
|
||||
});
|
||||
}
|
||||
|
||||
// Catalog-entry shape for the download modal, built from an installed model's
|
||||
// stored config so the same modal handles a precision upgrade.
|
||||
function entryForGroup(group) {
|
||||
const s = modelSettings[group.versions[0]?.hash] || {};
|
||||
const entry = {
|
||||
hash: group.versions[0]?.hash,
|
||||
display_name: s.display_name || group.name,
|
||||
model_name: s.model_name || group.name,
|
||||
version: s.version || "",
|
||||
architecture: s.architecture || group.architecture || "",
|
||||
family: s.family || group.family || "",
|
||||
description: s.description || "",
|
||||
files: s.files || [],
|
||||
};
|
||||
if (s.civitai_model_id) entry.civitai_model_id = s.civitai_model_id;
|
||||
if (s.thumbnail) entry.thumbnail = s.thumbnail;
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Installed precision tag → that precision's templates.
|
||||
function handleInstalledPrecisionClick(prec) {
|
||||
if (prec.model && templatesForModel(prec.model).length > 0) {
|
||||
activeNestedSubmenu = null;
|
||||
activeSubmenu = {
|
||||
type: "template",
|
||||
model: prec.model,
|
||||
displayName: modelSettings[prec.model.hash]?.display_name || prec.model.filename,
|
||||
};
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
// Missing precision tag → download modal pre-set to that precision.
|
||||
function handleMissingPrecisionClick(group, prec) {
|
||||
onShowCatalogDownload(entryForGroup(group), prec.precision);
|
||||
}
|
||||
|
||||
function sortCategories(cats) {
|
||||
const known = (categoryOrder || []).filter(c => cats.includes(c));
|
||||
const unknown = cats.filter(c => !categoryOrder?.includes(c)).sort();
|
||||
@@ -464,26 +519,48 @@
|
||||
const version = settings?.version || "";
|
||||
const filename = (model.filename || "").toLowerCase();
|
||||
|
||||
// No separator requirement — civitai filenames embed precision in
|
||||
// camelCase (v4BaseFp8) which lowercasing flattens to "basefp8".
|
||||
// Prefer the precision the config DECLARES for this exact file variant.
|
||||
// A filename regex collapses GGUF quants (Q4/Q5/Q8 all read "GGUF") and
|
||||
// fp8_scaled vs fp8, so multiple installed precisions of one model would
|
||||
// render as identical, indistinguishable rows. The variant's precision
|
||||
// field disambiguates them. Fall back to a filename sniff for configs
|
||||
// with no variants array.
|
||||
const precision = declaredPrecision(settings, filename) || sniffPrecision(filename);
|
||||
|
||||
if (precision && version) {
|
||||
const esc = precision.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
if (version.toLowerCase().includes(precision.toLowerCase())) {
|
||||
return version.replace(new RegExp(`\\s*${esc}`, "i"), ` (${precision})`);
|
||||
}
|
||||
return `${version} (${precision})`;
|
||||
}
|
||||
return version || model.filename || "";
|
||||
}
|
||||
|
||||
// The precision label a config declares for the file with this exact name,
|
||||
// checking each file entry's flat filename and its precision variants.
|
||||
function declaredPrecision(settings, filenameLower) {
|
||||
for (const f of settings?.files || []) {
|
||||
if ((f.filename || "").toLowerCase() === filenameLower) return f.precision || null;
|
||||
for (const v of f.variants || []) {
|
||||
if ((v.filename || "").toLowerCase() === filenameLower) return v.precision || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// No separator requirement — civitai filenames embed precision in
|
||||
// camelCase (v4BaseFp8) which lowercasing flattens to "basefp8".
|
||||
function sniffPrecision(filenameLower) {
|
||||
const precisionPatterns = [
|
||||
[/bf16/i, "BF16"], [/fp16/i, "FP16"], [/fp8/i, "FP8"],
|
||||
[/fp32/i, "FP32"], [/nvfp4/i, "NVFP4"], [/fp4/i, "FP4"],
|
||||
[/\.gguf$/i, "GGUF"],
|
||||
];
|
||||
|
||||
let precision = null;
|
||||
for (const [pattern, label] of precisionPatterns) {
|
||||
if (pattern.test(filename)) { precision = label; break; }
|
||||
if (pattern.test(filenameLower)) return label;
|
||||
}
|
||||
|
||||
if (precision && version) {
|
||||
if (version.toLowerCase().includes(precision.toLowerCase())) {
|
||||
return version.replace(new RegExp(`\\s*${precision}`, "i"), ` (${precision})`);
|
||||
}
|
||||
return `${version} (${precision})`;
|
||||
}
|
||||
return version || model.filename || "";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Catalog entries have no modelSettings[hash], so derive the version label
|
||||
@@ -683,6 +760,7 @@
|
||||
{@const model = group.versions[0]}
|
||||
{@const displayName = group.name}
|
||||
{@const meta = [group.architecture, group.family].filter(Boolean).join(" · ")}
|
||||
{@const precs = groupPrecisions(group)}
|
||||
<div class="pcr-picker-item"
|
||||
class:pcr-picker-item-active={activeSubmenu?.type === "version" && activeSubmenu?.group?.name === group.name
|
||||
|| activeSubmenu?.type === "template" && activeSubmenu?.model?.hash === model.hash}
|
||||
@@ -693,42 +771,63 @@
|
||||
<div class="pcr-picker-item-info">
|
||||
<span class="pcr-picker-item-name">
|
||||
{displayName}
|
||||
{#if group.versions.length > 1}
|
||||
{#if group.versions.length > 1 && !precs.length}
|
||||
<span class="pcr-model-version-badge">{group.versions.length}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if meta}
|
||||
<span class="pcr-picker-item-meta">{meta}</span>
|
||||
{/if}
|
||||
{#if precs.length}
|
||||
<div class="pcr-picker-precision-row">
|
||||
{#each precs as p}
|
||||
<span class="pcr-picker-precision-tag"
|
||||
class:pcr-precision-installed={p.installed}
|
||||
class:pcr-precision-missing={!p.installed}
|
||||
title={p.installed ? `${p.precision.toUpperCase()} installed — use it` : `Download ${p.precision.toUpperCase()}`}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
p.installed ? handleInstalledPrecisionClick(p) : handleMissingPrecisionClick(group, p);
|
||||
}}>
|
||||
{p.precision.toUpperCase()}{p.installed ? "" : " ↓"}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="pcr-picker-item-arrow">▸</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Partially installed catalog (with inline precision buttons) -->
|
||||
<!-- Started catalog entries (installed / resumable-partial precisions) -->
|
||||
{#each catalogInstalled as entry}
|
||||
{@const precisions = extractPrecisions(entry.files || [])}
|
||||
{@const status = catalogPrecisionStatus[entry.hash] || {}}
|
||||
<div class="pcr-picker-item pcr-picker-catalog-item"
|
||||
class:pcr-picker-catalog-partial={precisions.some(p => status[p] === "partial")}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
const installedPrec = precisions.find(p => status[p]);
|
||||
if (installedPrec) handleCatalogPrecisionClick(entry, installedPrec, true);
|
||||
const prec = precisions.find(p => status[p] === "installed")
|
||||
|| precisions.find(p => status[p] === "partial");
|
||||
if (prec) handleCatalogPrecisionClick(entry, prec, status[prec]);
|
||||
}}>
|
||||
<div class="pcr-picker-item-info">
|
||||
<span class="pcr-picker-item-name">{entry.display_name || entry.model_name}</span>
|
||||
<div class="pcr-picker-precision-row">
|
||||
{#each precisions as prec}
|
||||
{@const installed = !!status[prec]}
|
||||
{@const st = status[prec] || "missing"}
|
||||
<span class="pcr-picker-precision-tag"
|
||||
class:pcr-precision-installed={installed}
|
||||
class:pcr-precision-missing={!installed}
|
||||
title={installed ? `${prec.toUpperCase()} installed` : `Download ${prec.toUpperCase()}`}
|
||||
class:pcr-precision-installed={st === "installed"}
|
||||
class:pcr-precision-partial={st === "partial"}
|
||||
class:pcr-precision-missing={st === "missing"}
|
||||
title={st === "installed" ? `${prec.toUpperCase()} installed`
|
||||
: st === "partial" ? `${prec.toUpperCase()} download unfinished — click to resume`
|
||||
: `Download ${prec.toUpperCase()}`}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCatalogPrecisionClick(entry, prec, installed);
|
||||
handleCatalogPrecisionClick(entry, prec, st);
|
||||
}}>
|
||||
{prec.toUpperCase()}{installed ? "" : " ↓"}
|
||||
{prec.toUpperCase()}{st === "installed" ? "" : st === "partial" ? " ↻" : " ↓"}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -744,6 +843,7 @@
|
||||
{@const meta = [group.architecture, group.family].filter(Boolean).join(" · ")}
|
||||
<div class="pcr-picker-item pcr-picker-catalog-item"
|
||||
class:pcr-picker-item-active={activeSubmenu?.type === "catalogVersion" && activeSubmenu?.group === group}
|
||||
class:pcr-picker-catalog-partial={group.partial}
|
||||
onclick={(e) => { e.stopPropagation(); handleCatalogGroupClick(group); }}>
|
||||
<div class="pcr-picker-item-info">
|
||||
<span class="pcr-picker-item-name">
|
||||
@@ -752,11 +852,13 @@
|
||||
<span class="pcr-model-version-badge">{group.versions.length}</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if meta}
|
||||
<span class="pcr-picker-item-meta">{meta}</span>
|
||||
{#if meta || group.partial}
|
||||
<span class="pcr-picker-item-meta">
|
||||
{meta}{#if group.partial}{meta ? " · " : ""}<span class="pcr-picker-resume-hint">resume ↻</span>{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="pcr-picker-item-arrow">{group.versions.length > 1 ? "▸" : "↓"}</span>
|
||||
<span class="pcr-picker-item-arrow">{group.versions.length > 1 ? "▸" : (group.partial ? "↻" : "↓")}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -1103,6 +1205,14 @@
|
||||
.pcr-precision-installed:hover {
|
||||
background: rgba(79, 195, 247, 0.25);
|
||||
}
|
||||
.pcr-precision-partial {
|
||||
background: rgba(255, 183, 77, 0.15);
|
||||
color: #ffb74d;
|
||||
border: 1px solid rgba(255, 183, 77, 0.4);
|
||||
}
|
||||
.pcr-precision-partial:hover {
|
||||
background: rgba(255, 183, 77, 0.28);
|
||||
}
|
||||
.pcr-precision-missing {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #888;
|
||||
@@ -1112,6 +1222,14 @@
|
||||
color: #ccc;
|
||||
border-color: #666;
|
||||
}
|
||||
/* a started-but-unfinished download gets an amber edge so it reads as
|
||||
"resume", not "ready" */
|
||||
.pcr-picker-catalog-partial {
|
||||
border-left-color: #ffb74d;
|
||||
}
|
||||
.pcr-picker-resume-hint {
|
||||
color: #ffb74d;
|
||||
}
|
||||
|
||||
.pcr-picker-civitai-loading {
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -130,6 +130,18 @@
|
||||
next.delete(itemPath);
|
||||
flashingThumbs = next;
|
||||
}
|
||||
|
||||
// A cover whose /thumb request raced the record commit latches broken with no
|
||||
// recovery. Cache-bust and re-fetch on the error event — bounded, no polling.
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pcr-gv" bind:this={wrapEl}>
|
||||
@@ -183,7 +195,7 @@
|
||||
{/if}
|
||||
{:else if item.type === "workflow" && item.thumbnailHash}
|
||||
<img src={thumbSrc(item)} alt={item.name} loading="lazy" draggable="false"
|
||||
onload={() => onThumbUpdate(item)} />
|
||||
onload={() => onThumbUpdate(item)} onerror={retryThumb} />
|
||||
{#if flashingThumbs.has(item.path)}
|
||||
<div class="pcr-ji-flash" onanimationend={() => onFlashEnd(item.path)}></div>
|
||||
{/if}
|
||||
|
||||
@@ -24,6 +24,18 @@
|
||||
thumbLoaded = true;
|
||||
}
|
||||
|
||||
// A cover whose /thumb request raced the record commit latches broken with no
|
||||
// recovery. Cache-bust and re-fetch on the error event — bounded, no polling.
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
if (item.type !== "folder") return;
|
||||
if (!e.dataTransfer.types.includes("application/x-promptchain-move")) return;
|
||||
@@ -81,6 +93,7 @@
|
||||
src={apiURL(`/promptchain/thumb/${item.thumbnailHash}`)}
|
||||
alt={item.name} loading="lazy" draggable="false"
|
||||
onload={onThumbUpdate}
|
||||
onerror={retryThumb}
|
||||
/>
|
||||
{#if showFlash}
|
||||
<div class="pcr-gi-flash" onanimationend={() => showFlash = false}></div>
|
||||
|
||||
@@ -27,6 +27,18 @@
|
||||
return (b / 1048576).toFixed(1) + " MB";
|
||||
}
|
||||
|
||||
// A cover whose /thumb request raced the record commit latches broken with no
|
||||
// recovery. Cache-bust and re-fetch on the error event — bounded, no polling.
|
||||
function retryThumb(e) {
|
||||
const img = e.currentTarget;
|
||||
const n = +img.dataset.pcrRetry || 0;
|
||||
if (n >= 3) return;
|
||||
img.dataset.pcrRetry = String(n + 1);
|
||||
const u = new URL(img.src, location.href);
|
||||
u.searchParams.set("r", String(n + 1));
|
||||
img.src = u.toString();
|
||||
}
|
||||
|
||||
function fmtDate(ts) {
|
||||
if (!ts) return "-";
|
||||
return new Date(ts * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
@@ -122,6 +134,7 @@
|
||||
class="pcr-lv-mini"
|
||||
src={apiURL(`/promptchain/thumb/${item.thumbnailHash}`)}
|
||||
alt="" loading="lazy"
|
||||
onerror={retryThumb}
|
||||
/>
|
||||
{:else if item.type === "workflow"}
|
||||
<svg class="pcr-lv-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user